squarified 0.1.0
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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/index.d.ts +283 -0
- package/dist/index.js +1438 -0
- package/dist/index.mjs +1430 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1438 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var _computedKey;
|
|
4
|
+
_computedKey = Symbol.iterator;
|
|
5
|
+
class Iter {
|
|
6
|
+
keys;
|
|
7
|
+
data;
|
|
8
|
+
constructor(data){
|
|
9
|
+
this.data = data;
|
|
10
|
+
this.keys = Object.keys(data);
|
|
11
|
+
}
|
|
12
|
+
// dprint-ignore
|
|
13
|
+
*[_computedKey]() {
|
|
14
|
+
for(let i = 0; i < this.keys.length; i++){
|
|
15
|
+
yield {
|
|
16
|
+
key: this.keys[i],
|
|
17
|
+
value: this.data[this.keys[i]],
|
|
18
|
+
index: i,
|
|
19
|
+
peek: ()=>this.keys[i + 1]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// For strings we only check the first character to determine if it's a number (I think it's enough)
|
|
25
|
+
function perferNumeric(s) {
|
|
26
|
+
if (typeof s === 'number') return true;
|
|
27
|
+
return s.charCodeAt(0) >= 48 && s.charCodeAt(0) <= 57;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
31
|
+
class Matrix2D {
|
|
32
|
+
a;
|
|
33
|
+
b;
|
|
34
|
+
c;
|
|
35
|
+
d;
|
|
36
|
+
e;
|
|
37
|
+
f;
|
|
38
|
+
constructor(loc = {}){
|
|
39
|
+
this.a = loc.a || 1;
|
|
40
|
+
this.b = loc.b || 0;
|
|
41
|
+
this.c = loc.c || 0;
|
|
42
|
+
this.d = loc.d || 1;
|
|
43
|
+
this.e = loc.e || 0;
|
|
44
|
+
this.f = loc.f || 0;
|
|
45
|
+
}
|
|
46
|
+
create(loc) {
|
|
47
|
+
for (const { key, value } of new Iter(loc)){
|
|
48
|
+
if (Object.hasOwnProperty.call(this, key)) {
|
|
49
|
+
this[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
transform(x, y, scaleX, scaleY, rotation, skewX, skewY) {
|
|
55
|
+
this.scale(scaleX, scaleY).translation(x, y);
|
|
56
|
+
if (skewX || skewY) {
|
|
57
|
+
this.skew(skewX, skewY);
|
|
58
|
+
} else {
|
|
59
|
+
this.roate(rotation);
|
|
60
|
+
}
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
translation(x, y) {
|
|
64
|
+
this.e += x;
|
|
65
|
+
this.f += y;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
scale(a, d) {
|
|
69
|
+
this.a *= a;
|
|
70
|
+
this.d *= d;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
skew(x, y) {
|
|
74
|
+
const tanX = Math.tan(x * DEG_TO_RAD);
|
|
75
|
+
const tanY = Math.tan(y * DEG_TO_RAD);
|
|
76
|
+
const a = this.a + this.b * tanX;
|
|
77
|
+
const b = this.b + this.a * tanY;
|
|
78
|
+
const c = this.c + this.d * tanX;
|
|
79
|
+
const d = this.d + this.c * tanY;
|
|
80
|
+
this.a = a;
|
|
81
|
+
this.b = b;
|
|
82
|
+
this.c = c;
|
|
83
|
+
this.d = d;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
roate(rotation) {
|
|
87
|
+
if (rotation > 0) {
|
|
88
|
+
const rad = rotation * DEG_TO_RAD;
|
|
89
|
+
const cosTheta = Math.cos(rad);
|
|
90
|
+
const sinTheta = Math.sin(rad);
|
|
91
|
+
const a = this.a * cosTheta - this.b * sinTheta;
|
|
92
|
+
const b = this.a * sinTheta + this.b * cosTheta;
|
|
93
|
+
const c = this.c * cosTheta - this.d * sinTheta;
|
|
94
|
+
const d = this.c * sinTheta + this.d * cosTheta;
|
|
95
|
+
this.a = a;
|
|
96
|
+
this.b = b;
|
|
97
|
+
this.c = c;
|
|
98
|
+
this.d = d;
|
|
99
|
+
}
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const SELF_ID = {
|
|
105
|
+
id: 0,
|
|
106
|
+
get () {
|
|
107
|
+
return this.id++;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
class Display {
|
|
111
|
+
parent;
|
|
112
|
+
id;
|
|
113
|
+
matrix;
|
|
114
|
+
constructor(){
|
|
115
|
+
this.parent = null;
|
|
116
|
+
this.id = SELF_ID.get();
|
|
117
|
+
this.matrix = new Matrix2D();
|
|
118
|
+
}
|
|
119
|
+
destory() {
|
|
120
|
+
//
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const ASSIGN_MAPPINGS = {
|
|
124
|
+
fillStyle: !0,
|
|
125
|
+
strokeStyle: !0,
|
|
126
|
+
font: !0,
|
|
127
|
+
lineWidth: !0,
|
|
128
|
+
textAlign: !0,
|
|
129
|
+
textBaseline: !0
|
|
130
|
+
};
|
|
131
|
+
function createInstruction() {
|
|
132
|
+
return {
|
|
133
|
+
mods: [],
|
|
134
|
+
fillStyle (...args) {
|
|
135
|
+
this.mods.push([
|
|
136
|
+
'fillStyle',
|
|
137
|
+
args
|
|
138
|
+
]);
|
|
139
|
+
},
|
|
140
|
+
fillRect (...args) {
|
|
141
|
+
this.mods.push([
|
|
142
|
+
'fillRect',
|
|
143
|
+
args
|
|
144
|
+
]);
|
|
145
|
+
},
|
|
146
|
+
strokeStyle (...args) {
|
|
147
|
+
this.mods.push([
|
|
148
|
+
'strokeStyle',
|
|
149
|
+
args
|
|
150
|
+
]);
|
|
151
|
+
},
|
|
152
|
+
lineWidth (...args) {
|
|
153
|
+
this.mods.push([
|
|
154
|
+
'lineWidth',
|
|
155
|
+
args
|
|
156
|
+
]);
|
|
157
|
+
},
|
|
158
|
+
strokeRect (...args) {
|
|
159
|
+
this.mods.push([
|
|
160
|
+
'strokeRect',
|
|
161
|
+
args
|
|
162
|
+
]);
|
|
163
|
+
},
|
|
164
|
+
fillText (...args) {
|
|
165
|
+
this.mods.push([
|
|
166
|
+
'fillText',
|
|
167
|
+
args
|
|
168
|
+
]);
|
|
169
|
+
},
|
|
170
|
+
font (...args) {
|
|
171
|
+
this.mods.push([
|
|
172
|
+
'font',
|
|
173
|
+
args
|
|
174
|
+
]);
|
|
175
|
+
},
|
|
176
|
+
textBaseline (...args) {
|
|
177
|
+
this.mods.push([
|
|
178
|
+
'textBaseline',
|
|
179
|
+
args
|
|
180
|
+
]);
|
|
181
|
+
},
|
|
182
|
+
textAlign (...args) {
|
|
183
|
+
this.mods.push([
|
|
184
|
+
'textAlign',
|
|
185
|
+
args
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
class S extends Display {
|
|
191
|
+
width;
|
|
192
|
+
height;
|
|
193
|
+
x;
|
|
194
|
+
y;
|
|
195
|
+
scaleX;
|
|
196
|
+
scaleY;
|
|
197
|
+
rotation;
|
|
198
|
+
skewX;
|
|
199
|
+
skewY;
|
|
200
|
+
constructor(options = {}){
|
|
201
|
+
super();
|
|
202
|
+
this.width = options.width || 0;
|
|
203
|
+
this.height = options.height || 0;
|
|
204
|
+
this.x = options.x || 0;
|
|
205
|
+
this.y = options.y || 0;
|
|
206
|
+
this.scaleX = options.scaleX || 1;
|
|
207
|
+
this.scaleY = options.scaleY || 1;
|
|
208
|
+
this.rotation = options.rotation || 0;
|
|
209
|
+
this.skewX = options.skewX || 0;
|
|
210
|
+
this.skewY = options.skewY || 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
class Graph extends S {
|
|
214
|
+
instruction;
|
|
215
|
+
constructor(options = {}){
|
|
216
|
+
super(options);
|
|
217
|
+
this.instruction = createInstruction();
|
|
218
|
+
}
|
|
219
|
+
render(ctx) {
|
|
220
|
+
this.create();
|
|
221
|
+
this.instruction.mods.forEach((mod)=>{
|
|
222
|
+
const direct = mod[0];
|
|
223
|
+
if (direct in ASSIGN_MAPPINGS) {
|
|
224
|
+
// @ts-expect-error
|
|
225
|
+
ctx[direct] = mod[1];
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// @ts-expect-error
|
|
229
|
+
ctx[direct].apply(ctx, ...mod.slice(1));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class Box extends Display {
|
|
235
|
+
elements;
|
|
236
|
+
constructor(){
|
|
237
|
+
super();
|
|
238
|
+
this.elements = [];
|
|
239
|
+
}
|
|
240
|
+
add(...elements) {
|
|
241
|
+
const cap = elements.length;
|
|
242
|
+
for(let i = 0; i < cap; i++){
|
|
243
|
+
const element = elements[i];
|
|
244
|
+
if (element.parent) ;
|
|
245
|
+
this.elements.push(element);
|
|
246
|
+
element.parent = this;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
remove(...elements) {
|
|
250
|
+
const cap = elements.length;
|
|
251
|
+
for(let i = 0; i < cap; i++){
|
|
252
|
+
for(let j = this.elements.length - 1; j >= 0; j--){
|
|
253
|
+
const element = this.elements[j];
|
|
254
|
+
if (element.id === elements[i].id) {
|
|
255
|
+
this.elements.splice(j, 1);
|
|
256
|
+
element.parent = null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
destory() {
|
|
262
|
+
this.elements.forEach((element)=>element.parent = null);
|
|
263
|
+
this.elements.length = 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Runtime is designed for graph element
|
|
268
|
+
function decodeHLS(meta) {
|
|
269
|
+
const { h, l, s, a } = meta;
|
|
270
|
+
if ('a' in meta) {
|
|
271
|
+
return `hsla(${h}deg, ${s}%, ${l}%, ${a})`;
|
|
272
|
+
}
|
|
273
|
+
return `hsl(${h}deg, ${s}%, ${l}%)`;
|
|
274
|
+
}
|
|
275
|
+
function decodeRGB(meta) {
|
|
276
|
+
const { r, g, b, a } = meta;
|
|
277
|
+
if ('a' in meta) {
|
|
278
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
279
|
+
}
|
|
280
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
281
|
+
}
|
|
282
|
+
function decodeColor(meta) {
|
|
283
|
+
return meta.mode === 'rgb' ? decodeRGB(meta.desc) : decodeHLS(meta.desc);
|
|
284
|
+
}
|
|
285
|
+
function evaluateFillStyle(primitive, opacity = 1) {
|
|
286
|
+
const descibe = {
|
|
287
|
+
mode: primitive.mode,
|
|
288
|
+
desc: {
|
|
289
|
+
...primitive.desc,
|
|
290
|
+
a: opacity
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
return decodeColor(descibe);
|
|
294
|
+
}
|
|
295
|
+
const runtime = {
|
|
296
|
+
evaluateFillStyle
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
class Rect extends Graph {
|
|
300
|
+
style;
|
|
301
|
+
constructor(options = {}){
|
|
302
|
+
super(options);
|
|
303
|
+
this.style = options.style || Object.create(null);
|
|
304
|
+
}
|
|
305
|
+
create() {
|
|
306
|
+
if (this.style.fill) {
|
|
307
|
+
this.instruction.fillStyle(runtime.evaluateFillStyle(this.style.fill, this.style.opacity));
|
|
308
|
+
this.instruction.fillRect(0, 0, this.width, this.height);
|
|
309
|
+
}
|
|
310
|
+
if (this.style.stroke) {
|
|
311
|
+
this.instruction.strokeStyle(this.style.stroke);
|
|
312
|
+
if (typeof this.style.lineWidth === 'number') {
|
|
313
|
+
this.instruction.lineWidth(this.style.lineWidth);
|
|
314
|
+
}
|
|
315
|
+
this.instruction.strokeRect(0, 0, this.width, this.height);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
class Text extends Graph {
|
|
321
|
+
text;
|
|
322
|
+
style;
|
|
323
|
+
constructor(options = {}){
|
|
324
|
+
super(options);
|
|
325
|
+
this.text = options.text || '';
|
|
326
|
+
this.style = options.style || Object.create(null);
|
|
327
|
+
}
|
|
328
|
+
create() {
|
|
329
|
+
if (this.style.fill) {
|
|
330
|
+
this.instruction.font(this.style.font);
|
|
331
|
+
this.instruction.lineWidth(this.style.lineWidth);
|
|
332
|
+
this.instruction.textBaseline(this.style.baseline);
|
|
333
|
+
this.instruction.fillStyle(this.style.fill);
|
|
334
|
+
this.instruction.fillText(this.text, 0, 0);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
class Event {
|
|
340
|
+
eventCollections;
|
|
341
|
+
constructor(){
|
|
342
|
+
this.eventCollections = Object.create(null);
|
|
343
|
+
}
|
|
344
|
+
on(evt, handler, c) {
|
|
345
|
+
if (!(evt in this.eventCollections)) {
|
|
346
|
+
this.eventCollections[evt] = [];
|
|
347
|
+
}
|
|
348
|
+
const data = {
|
|
349
|
+
name: evt,
|
|
350
|
+
handler,
|
|
351
|
+
ctx: c || this
|
|
352
|
+
};
|
|
353
|
+
this.eventCollections[evt].push(data);
|
|
354
|
+
}
|
|
355
|
+
off(evt, handler) {
|
|
356
|
+
if (evt in this.eventCollections) {
|
|
357
|
+
if (!handler) {
|
|
358
|
+
this.eventCollections[evt] = [];
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.eventCollections[evt] = this.eventCollections[evt].filter((d)=>d.handler !== handler);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
emit(evt, ...args) {
|
|
365
|
+
if (!this.eventCollections[evt]) return;
|
|
366
|
+
const handlers = this.eventCollections[evt];
|
|
367
|
+
if (handlers.length) {
|
|
368
|
+
handlers.forEach((d)=>{
|
|
369
|
+
d.handler.call(d.ctx, ...args);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
bindWithContext(c) {
|
|
374
|
+
return (evt, handler)=>this.on(evt, handler, c);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const NAME_SPACE = 'etoile';
|
|
379
|
+
const log = {
|
|
380
|
+
error: (message)=>{
|
|
381
|
+
return `[${NAME_SPACE}] ${message}`;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
class Render {
|
|
386
|
+
canvas;
|
|
387
|
+
ctx;
|
|
388
|
+
options;
|
|
389
|
+
constructor(to, options){
|
|
390
|
+
this.canvas = document.createElement('canvas');
|
|
391
|
+
this.ctx = this.canvas.getContext('2d');
|
|
392
|
+
this.options = options;
|
|
393
|
+
this.initOptions(options);
|
|
394
|
+
to.appendChild(this.canvas);
|
|
395
|
+
}
|
|
396
|
+
clear(width, height) {
|
|
397
|
+
this.ctx.clearRect(0, 0, width, height);
|
|
398
|
+
}
|
|
399
|
+
initOptions(userOptions = {}) {
|
|
400
|
+
Object.assign(this.options, userOptions);
|
|
401
|
+
const { options } = this;
|
|
402
|
+
this.canvas.width = options.width * options.devicePixelRatio;
|
|
403
|
+
this.canvas.height = options.height * options.devicePixelRatio;
|
|
404
|
+
this.canvas.style.cssText = `width: ${options.width}px; height: ${options.height}px`;
|
|
405
|
+
}
|
|
406
|
+
update(schedule) {
|
|
407
|
+
this.clear(this.options.width, this.options.height);
|
|
408
|
+
schedule.execute(this);
|
|
409
|
+
}
|
|
410
|
+
destory() {}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let Schedule$1 = class Schedule extends Box {
|
|
414
|
+
render;
|
|
415
|
+
to;
|
|
416
|
+
event;
|
|
417
|
+
constructor(to, renderOptions = {}){
|
|
418
|
+
super();
|
|
419
|
+
this.to = typeof to === 'string' ? document.querySelector(to) : to;
|
|
420
|
+
if (!this.to) {
|
|
421
|
+
throw new Error(log.error('The element to bind is not found.'));
|
|
422
|
+
}
|
|
423
|
+
const { width, height } = this.to.getBoundingClientRect();
|
|
424
|
+
Object.assign(renderOptions, {
|
|
425
|
+
width,
|
|
426
|
+
height
|
|
427
|
+
}, {
|
|
428
|
+
devicePixelRatio: window.devicePixelRatio || 1
|
|
429
|
+
});
|
|
430
|
+
this.event = new Event();
|
|
431
|
+
this.render = new Render(this.to, renderOptions);
|
|
432
|
+
}
|
|
433
|
+
applyTransform(matrix) {
|
|
434
|
+
const pixel = this.render.options.devicePixelRatio;
|
|
435
|
+
this.render.ctx.setTransform(matrix.a * pixel, matrix.b * pixel, matrix.c * pixel, matrix.d * pixel, matrix.e * pixel, matrix.f * pixel);
|
|
436
|
+
}
|
|
437
|
+
update() {
|
|
438
|
+
this.render.update(this);
|
|
439
|
+
const matrix = this.matrix.create({
|
|
440
|
+
a: 1,
|
|
441
|
+
b: 0,
|
|
442
|
+
c: 0,
|
|
443
|
+
d: 1,
|
|
444
|
+
e: 0,
|
|
445
|
+
f: 0
|
|
446
|
+
});
|
|
447
|
+
this.applyTransform(matrix);
|
|
448
|
+
}
|
|
449
|
+
// execute all graph elements
|
|
450
|
+
execute(render, graph = this) {
|
|
451
|
+
render.ctx.save();
|
|
452
|
+
let matrix = graph.matrix;
|
|
453
|
+
this.applyTransform(matrix);
|
|
454
|
+
if (graph instanceof Box) {
|
|
455
|
+
const cap = graph.elements.length;
|
|
456
|
+
for(let i = 0; i < cap; i++){
|
|
457
|
+
const element = graph.elements[i];
|
|
458
|
+
matrix = element.matrix.create({
|
|
459
|
+
a: 1,
|
|
460
|
+
b: 0,
|
|
461
|
+
c: 0,
|
|
462
|
+
d: 1,
|
|
463
|
+
e: 0,
|
|
464
|
+
f: 0
|
|
465
|
+
});
|
|
466
|
+
if (element instanceof Graph) {
|
|
467
|
+
matrix.transform(element.x, element.y, element.scaleX, element.scaleY, element.rotation, element.skewX, element.skewY);
|
|
468
|
+
}
|
|
469
|
+
this.execute(render, element);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (graph instanceof Graph) {
|
|
473
|
+
graph.render(render.ctx);
|
|
474
|
+
}
|
|
475
|
+
render.ctx.restore();
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
function traverse(graphs, handler) {
|
|
480
|
+
graphs.forEach((graph)=>{
|
|
481
|
+
if (graph instanceof Box) {
|
|
482
|
+
traverse(graph.elements, handler);
|
|
483
|
+
} else if (graph instanceof Graph) {
|
|
484
|
+
handler(graph);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const etoile = {
|
|
489
|
+
Schedule: Schedule$1,
|
|
490
|
+
traverse
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Currently, etoile is an internal module, so we won't need too much easing functions.
|
|
494
|
+
// And the animation logic is implemented by user code.
|
|
495
|
+
const easing = {
|
|
496
|
+
linear: (k)=>k,
|
|
497
|
+
quadraticIn: (k)=>k * k,
|
|
498
|
+
quadraticOut: (k)=>k * (2 - k),
|
|
499
|
+
quadraticInOut: (k)=>{
|
|
500
|
+
if ((k *= 2) < 1) {
|
|
501
|
+
return 0.5 * k * k;
|
|
502
|
+
}
|
|
503
|
+
return -0.5 * (--k * (k - 2) - 1);
|
|
504
|
+
},
|
|
505
|
+
cubicIn: (k)=>k * k * k,
|
|
506
|
+
cubicOut: (k)=>{
|
|
507
|
+
if ((k *= 2) < 1) {
|
|
508
|
+
return 0.5 * k * k * k;
|
|
509
|
+
}
|
|
510
|
+
return 0.5 * ((k -= 2) * k * k + 2);
|
|
511
|
+
},
|
|
512
|
+
cubicInOut: (k)=>{
|
|
513
|
+
if ((k *= 2) < 1) {
|
|
514
|
+
return 0.5 * k * k * k;
|
|
515
|
+
}
|
|
516
|
+
return 0.5 * ((k -= 2) * k * k + 2);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
function sortChildrenByKey(data, ...keys) {
|
|
521
|
+
return data.sort((a, b)=>{
|
|
522
|
+
for (const key of keys){
|
|
523
|
+
const v = a[key];
|
|
524
|
+
const v2 = b[key];
|
|
525
|
+
if (perferNumeric(v) && perferNumeric(v2)) {
|
|
526
|
+
if (v2 > v) return 1;
|
|
527
|
+
if (v2 < v) return -1;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
// Not numeric, compare as string
|
|
531
|
+
const comparison = ('' + v).localeCompare('' + v2);
|
|
532
|
+
if (comparison !== 0) return comparison;
|
|
533
|
+
}
|
|
534
|
+
return 0;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function c2m(data, key, modifier) {
|
|
538
|
+
if (Array.isArray(data.groups)) {
|
|
539
|
+
data.groups = sortChildrenByKey(data.groups.map((d)=>c2m(d, key, modifier)), 'weight');
|
|
540
|
+
}
|
|
541
|
+
const obj = {
|
|
542
|
+
...data,
|
|
543
|
+
weight: data[key]
|
|
544
|
+
};
|
|
545
|
+
if (modifier) return modifier(obj);
|
|
546
|
+
return obj;
|
|
547
|
+
}
|
|
548
|
+
function flatten(data) {
|
|
549
|
+
const result = [];
|
|
550
|
+
for(let i = 0; i < data.length; i++){
|
|
551
|
+
const { groups, ...rest } = data[i];
|
|
552
|
+
result.push(rest);
|
|
553
|
+
if (groups) {
|
|
554
|
+
result.push(...flatten(groups));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
function bindParentForModule(modules, parent) {
|
|
560
|
+
return modules.map((module)=>{
|
|
561
|
+
const next = {
|
|
562
|
+
...module
|
|
563
|
+
};
|
|
564
|
+
next.parent = parent;
|
|
565
|
+
if (next.groups && Array.isArray(next.groups)) {
|
|
566
|
+
next.groups = bindParentForModule(next.groups, next);
|
|
567
|
+
}
|
|
568
|
+
return next;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function getNodeDepth(node) {
|
|
572
|
+
let depth = 0;
|
|
573
|
+
while(node.parent){
|
|
574
|
+
node = node.parent;
|
|
575
|
+
depth++;
|
|
576
|
+
}
|
|
577
|
+
return depth;
|
|
578
|
+
}
|
|
579
|
+
function visit(data, fn) {
|
|
580
|
+
if (!data) return null;
|
|
581
|
+
for (const d of data){
|
|
582
|
+
if (d.children) {
|
|
583
|
+
const result = visit(d.children, fn);
|
|
584
|
+
if (result) return result;
|
|
585
|
+
}
|
|
586
|
+
const stop = fn(d);
|
|
587
|
+
if (stop) return d;
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
function findRelativeNode(c, p, layoutNodes) {
|
|
592
|
+
return visit(layoutNodes, (node)=>{
|
|
593
|
+
const [x, y, w, h] = node.layout;
|
|
594
|
+
if (p.x >= x && p.y >= y && p.x < x + w && p.y < y + h) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function findRelativeNodeById(id, layoutNodes) {
|
|
600
|
+
return visit(layoutNodes, (node)=>{
|
|
601
|
+
if (node.node.id === id) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function squarify(data, rect, layoutDecorator) {
|
|
608
|
+
const result = [];
|
|
609
|
+
if (!data.length) return result;
|
|
610
|
+
const worst = (start, end, shortestSide, totalWeight, aspectRatio)=>{
|
|
611
|
+
const max = data[start].weight * aspectRatio;
|
|
612
|
+
const min = data[end].weight * aspectRatio;
|
|
613
|
+
return Math.max(shortestSide * shortestSide * max / (totalWeight * totalWeight), totalWeight * totalWeight / (shortestSide * shortestSide * min));
|
|
614
|
+
};
|
|
615
|
+
const recursion = (start, rect)=>{
|
|
616
|
+
while(start < data.length){
|
|
617
|
+
let totalWeight = 0;
|
|
618
|
+
for(let i = start; i < data.length; i++){
|
|
619
|
+
totalWeight += data[i].weight;
|
|
620
|
+
}
|
|
621
|
+
const shortestSide = Math.min(rect.w, rect.h);
|
|
622
|
+
const aspectRatio = rect.w * rect.h / totalWeight;
|
|
623
|
+
let end = start;
|
|
624
|
+
let areaInRun = 0;
|
|
625
|
+
let oldWorst = 0;
|
|
626
|
+
// find the best split
|
|
627
|
+
while(end < data.length){
|
|
628
|
+
const area = data[end].weight * aspectRatio;
|
|
629
|
+
const newWorst = worst(start, end, shortestSide, areaInRun + area, aspectRatio);
|
|
630
|
+
if (end > start && oldWorst < newWorst) break;
|
|
631
|
+
areaInRun += area;
|
|
632
|
+
oldWorst = newWorst;
|
|
633
|
+
end++;
|
|
634
|
+
}
|
|
635
|
+
const splited = Math.round(areaInRun / shortestSide);
|
|
636
|
+
let areaInLayout = 0;
|
|
637
|
+
for(let i = start; i < end; i++){
|
|
638
|
+
const children = data[i];
|
|
639
|
+
const area = children.weight * aspectRatio;
|
|
640
|
+
const lower = Math.round(shortestSide * areaInLayout / areaInRun);
|
|
641
|
+
const upper = Math.round(shortestSide * (areaInLayout + area) / areaInRun);
|
|
642
|
+
const [x, y, w, h] = rect.w >= rect.h ? [
|
|
643
|
+
rect.x,
|
|
644
|
+
rect.y + lower,
|
|
645
|
+
splited,
|
|
646
|
+
upper - lower
|
|
647
|
+
] : [
|
|
648
|
+
rect.x + lower,
|
|
649
|
+
rect.y,
|
|
650
|
+
upper - lower,
|
|
651
|
+
splited
|
|
652
|
+
];
|
|
653
|
+
const depth = getNodeDepth(children) || 1;
|
|
654
|
+
const { titleAreaHeight, rectGap } = layoutDecorator;
|
|
655
|
+
const diff = titleAreaHeight.max / depth;
|
|
656
|
+
const hh = diff < titleAreaHeight.min ? titleAreaHeight.min : diff;
|
|
657
|
+
result.push({
|
|
658
|
+
layout: [
|
|
659
|
+
x,
|
|
660
|
+
y,
|
|
661
|
+
w,
|
|
662
|
+
h
|
|
663
|
+
],
|
|
664
|
+
node: children,
|
|
665
|
+
decorator: {
|
|
666
|
+
...layoutDecorator,
|
|
667
|
+
titleHeight: hh
|
|
668
|
+
},
|
|
669
|
+
children: w > rectGap * 2 && h > hh + rectGap ? squarify(children.groups || [], {
|
|
670
|
+
x: x + rectGap,
|
|
671
|
+
y: y + hh,
|
|
672
|
+
w: w - rectGap * 2,
|
|
673
|
+
h: h - hh - rectGap
|
|
674
|
+
}, layoutDecorator) : []
|
|
675
|
+
});
|
|
676
|
+
areaInLayout += area;
|
|
677
|
+
}
|
|
678
|
+
start = end;
|
|
679
|
+
if (rect.w >= rect.h) {
|
|
680
|
+
rect.x += splited;
|
|
681
|
+
rect.w -= splited;
|
|
682
|
+
} else {
|
|
683
|
+
rect.y += splited;
|
|
684
|
+
rect.h -= splited;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
recursion(0, rect);
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function applyForOpacity(graph, lastState, nextState, easedProgress) {
|
|
693
|
+
const alpha = lastState + (nextState - lastState) * easedProgress;
|
|
694
|
+
if (graph instanceof Rect) {
|
|
695
|
+
graph.style.opacity = alpha;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
class RegisterModule {
|
|
700
|
+
static mixin(app, methods) {
|
|
701
|
+
methods.forEach(({ name, fn })=>{
|
|
702
|
+
Object.defineProperty(app, name, {
|
|
703
|
+
value: fn(app),
|
|
704
|
+
writable: false
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function registerModuleForSchedule(mod) {
|
|
710
|
+
if (mod instanceof RegisterModule) {
|
|
711
|
+
return (app, treemap, render)=>mod.init(app, treemap, render);
|
|
712
|
+
}
|
|
713
|
+
throw new Error(log.error('The module is not a valid RegisterScheduleModule.'));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// etoile is a simple 2D render engine for web and it don't take complex rendering into account.
|
|
717
|
+
// So it's no need to implement a complex event algorithm or hit mode.
|
|
718
|
+
// If one day etoile need to build as a useful library. Pls rewrite it!
|
|
719
|
+
// All of implementation don't want to consider the compatibility of the browser.
|
|
720
|
+
const primitiveEvents = [
|
|
721
|
+
'click',
|
|
722
|
+
'mousedown',
|
|
723
|
+
'mousemove',
|
|
724
|
+
'mouseup',
|
|
725
|
+
'mouseover',
|
|
726
|
+
'mouseout'
|
|
727
|
+
];
|
|
728
|
+
function smoothDrawing(c) {
|
|
729
|
+
const { self, treemap } = c;
|
|
730
|
+
const currentNode = self.currentNode;
|
|
731
|
+
if (currentNode) {
|
|
732
|
+
const lloc = new Set();
|
|
733
|
+
visit([
|
|
734
|
+
currentNode
|
|
735
|
+
], (node)=>{
|
|
736
|
+
const [x, y, w, h] = node.layout;
|
|
737
|
+
const { rectGap, titleHeight } = node.decorator;
|
|
738
|
+
lloc.add(x + '-' + y);
|
|
739
|
+
lloc.add(x + '-' + (y + h - rectGap));
|
|
740
|
+
lloc.add(x + '-' + (y + titleHeight));
|
|
741
|
+
lloc.add(x + w - rectGap + '-' + (y + titleHeight));
|
|
742
|
+
});
|
|
743
|
+
const startTime = Date.now();
|
|
744
|
+
const animationDuration = 300;
|
|
745
|
+
const draw = ()=>{
|
|
746
|
+
if (self.forceDestroy) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const elapsed = Date.now() - startTime;
|
|
750
|
+
const progress = Math.min(elapsed / animationDuration, 1);
|
|
751
|
+
const easedProgress = easing.cubicIn(progress) || 0.1;
|
|
752
|
+
let allTasksCompleted = true;
|
|
753
|
+
treemap.reset();
|
|
754
|
+
etoile.traverse([
|
|
755
|
+
treemap.elements[0]
|
|
756
|
+
], (graph)=>{
|
|
757
|
+
const key = `${graph.x}-${graph.y}`;
|
|
758
|
+
if (lloc.has(key)) {
|
|
759
|
+
applyForOpacity(graph, 1, 0.7, easedProgress);
|
|
760
|
+
if (progress < 1) {
|
|
761
|
+
allTasksCompleted = false;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
applyGraphTransform(treemap.elements, self.translateX, self.translateY, self.scaleRatio);
|
|
766
|
+
treemap.update();
|
|
767
|
+
if (!allTasksCompleted) {
|
|
768
|
+
window.requestAnimationFrame(draw);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
if (!self.isAnimating) {
|
|
772
|
+
self.isAnimating = true;
|
|
773
|
+
window.requestAnimationFrame(draw);
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
treemap.reset();
|
|
777
|
+
applyGraphTransform(treemap.elements, self.translateX, self.translateY, self.scaleRatio);
|
|
778
|
+
treemap.update();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function applyZoomEvent(ctx) {
|
|
782
|
+
ctx.treemap.event.on('zoom', (node)=>{
|
|
783
|
+
const root = null;
|
|
784
|
+
if (ctx.self.isDragging) return;
|
|
785
|
+
onZoom(ctx, node, root);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function getOffset(el) {
|
|
789
|
+
let e = 0;
|
|
790
|
+
let f = 0;
|
|
791
|
+
if (document.documentElement.getBoundingClientRect && el.getBoundingClientRect) {
|
|
792
|
+
const { top, left } = el.getBoundingClientRect();
|
|
793
|
+
e = top;
|
|
794
|
+
f = left;
|
|
795
|
+
} else {
|
|
796
|
+
for(let elt = el; elt; elt = el.offsetParent){
|
|
797
|
+
e += el.offsetLeft;
|
|
798
|
+
f += el.offsetTop;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return [
|
|
802
|
+
e + Math.max(document.documentElement.scrollLeft, document.body.scrollLeft),
|
|
803
|
+
f + Math.max(document.documentElement.scrollTop, document.body.scrollTop)
|
|
804
|
+
];
|
|
805
|
+
}
|
|
806
|
+
function captureBoxXY(c, evt, a, d, translateX, translateY) {
|
|
807
|
+
const boundingClientRect = c.getBoundingClientRect();
|
|
808
|
+
if (evt instanceof MouseEvent) {
|
|
809
|
+
const [e, f] = getOffset(c);
|
|
810
|
+
return {
|
|
811
|
+
x: (evt.clientX - boundingClientRect.left - e - translateX) / a,
|
|
812
|
+
y: (evt.clientY - boundingClientRect.top - f - translateY) / d
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
x: 0,
|
|
817
|
+
y: 0
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function bindPrimitiveEvent(ctx, evt, bus) {
|
|
821
|
+
const { treemap, self } = ctx;
|
|
822
|
+
const c = treemap.render.canvas;
|
|
823
|
+
const handler = (e)=>{
|
|
824
|
+
const { x, y } = captureBoxXY(c, e, self.scaleRatio, self.scaleRatio, self.translateX, self.translateY);
|
|
825
|
+
const event = {
|
|
826
|
+
native: e,
|
|
827
|
+
module: findRelativeNode(c, {
|
|
828
|
+
x,
|
|
829
|
+
y
|
|
830
|
+
}, treemap.layoutNodes)
|
|
831
|
+
};
|
|
832
|
+
// @ts-expect-error
|
|
833
|
+
bus.emit(evt, event);
|
|
834
|
+
};
|
|
835
|
+
c.addEventListener(evt, handler);
|
|
836
|
+
return handler;
|
|
837
|
+
}
|
|
838
|
+
class SelfEvent extends RegisterModule {
|
|
839
|
+
currentNode;
|
|
840
|
+
isAnimating;
|
|
841
|
+
forceDestroy;
|
|
842
|
+
scaleRatio;
|
|
843
|
+
translateX;
|
|
844
|
+
translateY;
|
|
845
|
+
layoutWidth;
|
|
846
|
+
layoutHeight;
|
|
847
|
+
isDragging;
|
|
848
|
+
draggingState;
|
|
849
|
+
event;
|
|
850
|
+
constructor(){
|
|
851
|
+
super();
|
|
852
|
+
this.currentNode = null;
|
|
853
|
+
this.isAnimating = false;
|
|
854
|
+
this.forceDestroy = false;
|
|
855
|
+
this.isDragging = false;
|
|
856
|
+
this.scaleRatio = 1;
|
|
857
|
+
this.translateX = 0;
|
|
858
|
+
this.translateY = 0;
|
|
859
|
+
this.layoutWidth = 0;
|
|
860
|
+
this.layoutHeight = 0;
|
|
861
|
+
this.draggingState = {
|
|
862
|
+
x: 0,
|
|
863
|
+
y: 0
|
|
864
|
+
};
|
|
865
|
+
this.event = new Event();
|
|
866
|
+
}
|
|
867
|
+
ondragstart(metadata) {
|
|
868
|
+
const { native } = metadata;
|
|
869
|
+
if (isScrollWheelOrRightButtonOnMouseupAndDown(native)) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const x = native.offsetX;
|
|
873
|
+
const y = native.offsetY;
|
|
874
|
+
this.self.isDragging = true;
|
|
875
|
+
this.self.draggingState = {
|
|
876
|
+
x,
|
|
877
|
+
y
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
ondragmove(metadata) {
|
|
881
|
+
if (!this.self.isDragging) {
|
|
882
|
+
if ('zoom' in this.treemap.event.eventCollections) {
|
|
883
|
+
const condit = this.treemap.event.eventCollections.zoom.length > 0;
|
|
884
|
+
if (!condit) {
|
|
885
|
+
applyZoomEvent(this);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
// @ts-expect-error
|
|
891
|
+
this.self.event.off('mousemove', this.self.onmousemove);
|
|
892
|
+
this.treemap.event.off('zoom');
|
|
893
|
+
this.self.forceDestroy = true;
|
|
894
|
+
const { native } = metadata;
|
|
895
|
+
const x = native.offsetX;
|
|
896
|
+
const y = native.offsetY;
|
|
897
|
+
const { x: lastX, y: lastY } = this.self.draggingState;
|
|
898
|
+
const drawX = x - lastX;
|
|
899
|
+
const drawY = y - lastY;
|
|
900
|
+
this.self.translateX += drawX;
|
|
901
|
+
this.self.translateY += drawY;
|
|
902
|
+
this.self.draggingState = {
|
|
903
|
+
x,
|
|
904
|
+
y
|
|
905
|
+
};
|
|
906
|
+
this.treemap.reset();
|
|
907
|
+
applyGraphTransform(this.treemap.elements, this.self.translateX, this.self.translateY, this.self.scaleRatio);
|
|
908
|
+
this.treemap.update();
|
|
909
|
+
}
|
|
910
|
+
ondragend(metadata) {
|
|
911
|
+
if (!this.self.isDragging) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
this.self.isDragging = false;
|
|
915
|
+
this.self.draggingState = {
|
|
916
|
+
x: 0,
|
|
917
|
+
y: 0
|
|
918
|
+
};
|
|
919
|
+
this.self.event.bindWithContext(this)('mousemove', this.self.onmousemove);
|
|
920
|
+
}
|
|
921
|
+
onmousemove(metadata) {
|
|
922
|
+
const { self } = this;
|
|
923
|
+
if (self.isDragging) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const { module: node } = metadata;
|
|
927
|
+
self.forceDestroy = false;
|
|
928
|
+
if (self.currentNode !== node) {
|
|
929
|
+
self.currentNode = node;
|
|
930
|
+
self.isAnimating = false;
|
|
931
|
+
}
|
|
932
|
+
smoothDrawing(this);
|
|
933
|
+
}
|
|
934
|
+
onmouseout() {
|
|
935
|
+
const { self } = this;
|
|
936
|
+
self.currentNode = null;
|
|
937
|
+
self.forceDestroy = true;
|
|
938
|
+
self.isDragging = false;
|
|
939
|
+
smoothDrawing(this);
|
|
940
|
+
}
|
|
941
|
+
onwheel(metadata) {
|
|
942
|
+
const { self, treemap } = this;
|
|
943
|
+
// @ts-expect-error
|
|
944
|
+
const wheelDelta = metadata.native.wheelDelta;
|
|
945
|
+
const absWheelDelta = Math.abs(wheelDelta);
|
|
946
|
+
const offsetX = metadata.native.offsetX;
|
|
947
|
+
const offsetY = metadata.native.offsetY;
|
|
948
|
+
if (wheelDelta === 0) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
self.forceDestroy = true;
|
|
952
|
+
self.isAnimating = true;
|
|
953
|
+
treemap.reset();
|
|
954
|
+
const factor = absWheelDelta > 3 ? 1.4 : absWheelDelta > 1 ? 1.2 : 1.1;
|
|
955
|
+
const delta = wheelDelta > 0 ? factor : 1 / factor;
|
|
956
|
+
self.scaleRatio *= delta;
|
|
957
|
+
const translateX = offsetX - (offsetX - self.translateX) * delta;
|
|
958
|
+
const translateY = offsetY - (offsetY - self.translateY) * delta;
|
|
959
|
+
self.translateX = translateX;
|
|
960
|
+
self.translateY = translateY;
|
|
961
|
+
applyGraphTransform(treemap.elements, self.translateX, self.translateY, self.scaleRatio);
|
|
962
|
+
treemap.update();
|
|
963
|
+
self.forceDestroy = false;
|
|
964
|
+
self.isAnimating = false;
|
|
965
|
+
}
|
|
966
|
+
init(app, treemap, render) {
|
|
967
|
+
const event = this.event;
|
|
968
|
+
const nativeEvents = [];
|
|
969
|
+
const methods = [
|
|
970
|
+
{
|
|
971
|
+
name: 'on',
|
|
972
|
+
fn: ()=>event.bindWithContext(treemap.api).bind(event)
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
name: 'off',
|
|
976
|
+
fn: ()=>event.off.bind(event)
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
name: 'emit',
|
|
980
|
+
fn: ()=>event.emit.bind(event)
|
|
981
|
+
}
|
|
982
|
+
];
|
|
983
|
+
RegisterModule.mixin(app, methods);
|
|
984
|
+
const selfEvents = [
|
|
985
|
+
...primitiveEvents,
|
|
986
|
+
'wheel'
|
|
987
|
+
];
|
|
988
|
+
selfEvents.forEach((evt)=>{
|
|
989
|
+
nativeEvents.push(bindPrimitiveEvent({
|
|
990
|
+
treemap,
|
|
991
|
+
self: this
|
|
992
|
+
}, evt, event));
|
|
993
|
+
});
|
|
994
|
+
const selfEvt = event.bindWithContext({
|
|
995
|
+
treemap,
|
|
996
|
+
self: this
|
|
997
|
+
});
|
|
998
|
+
selfEvt('mousedown', this.ondragstart);
|
|
999
|
+
selfEvt('mousemove', this.ondragmove);
|
|
1000
|
+
selfEvt('mouseup', this.ondragend);
|
|
1001
|
+
// highlight
|
|
1002
|
+
selfEvt('mousemove', this.onmousemove);
|
|
1003
|
+
selfEvt('mouseout', this.onmouseout);
|
|
1004
|
+
// wheel
|
|
1005
|
+
selfEvt('wheel', this.onwheel);
|
|
1006
|
+
applyZoomEvent({
|
|
1007
|
+
treemap,
|
|
1008
|
+
self: this
|
|
1009
|
+
});
|
|
1010
|
+
treemap.event.on('cleanup:selfevent', ()=>{
|
|
1011
|
+
this.currentNode = null;
|
|
1012
|
+
this.isAnimating = false;
|
|
1013
|
+
this.scaleRatio = 1;
|
|
1014
|
+
this.translateX = 0;
|
|
1015
|
+
this.translateY = 0;
|
|
1016
|
+
this.layoutWidth = treemap.render.canvas.width;
|
|
1017
|
+
this.layoutHeight = treemap.render.canvas.height;
|
|
1018
|
+
this.isDragging = false;
|
|
1019
|
+
this.draggingState = {
|
|
1020
|
+
x: 0,
|
|
1021
|
+
y: 0
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function estimateZoomingArea(node, root, w, h) {
|
|
1027
|
+
const defaultSizes = [
|
|
1028
|
+
w,
|
|
1029
|
+
h,
|
|
1030
|
+
1
|
|
1031
|
+
];
|
|
1032
|
+
if (root === node) {
|
|
1033
|
+
return defaultSizes;
|
|
1034
|
+
}
|
|
1035
|
+
const viewArea = w * h;
|
|
1036
|
+
let area = viewArea;
|
|
1037
|
+
let parent = node.node.parent;
|
|
1038
|
+
let totalWeight = node.node.weight;
|
|
1039
|
+
while(parent){
|
|
1040
|
+
const siblings = parent.groups || [];
|
|
1041
|
+
let siblingWeightSum = 0;
|
|
1042
|
+
for (const sibling of siblings){
|
|
1043
|
+
siblingWeightSum += sibling.weight;
|
|
1044
|
+
}
|
|
1045
|
+
area *= siblingWeightSum / totalWeight;
|
|
1046
|
+
totalWeight = parent.weight;
|
|
1047
|
+
parent = parent.parent;
|
|
1048
|
+
}
|
|
1049
|
+
const maxScaleFactor = 2.5;
|
|
1050
|
+
const minScaleFactor = 0.3;
|
|
1051
|
+
const scaleFactor = Math.max(minScaleFactor, Math.min(maxScaleFactor, Math.sqrt(area / viewArea)));
|
|
1052
|
+
return [
|
|
1053
|
+
w * scaleFactor,
|
|
1054
|
+
h * scaleFactor
|
|
1055
|
+
];
|
|
1056
|
+
}
|
|
1057
|
+
function applyGraphTransform(graphs, translateX, translateY, scale) {
|
|
1058
|
+
etoile.traverse(graphs, (graph)=>{
|
|
1059
|
+
graph.x = graph.x * scale + translateX;
|
|
1060
|
+
graph.y = graph.y * scale + translateY;
|
|
1061
|
+
graph.scaleX = scale;
|
|
1062
|
+
graph.scaleY = scale;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
function onZoom(ctx, node, root) {
|
|
1066
|
+
if (!node) return;
|
|
1067
|
+
const { treemap, self } = ctx;
|
|
1068
|
+
let isAnimating = false;
|
|
1069
|
+
const c = treemap.render.canvas;
|
|
1070
|
+
const boundingClientRect = c.getBoundingClientRect();
|
|
1071
|
+
const [w, h] = estimateZoomingArea(node, root, boundingClientRect.width, boundingClientRect.height);
|
|
1072
|
+
resetLayout(treemap, w, h);
|
|
1073
|
+
const module = findRelativeNodeById(node.node.id, treemap.layoutNodes);
|
|
1074
|
+
if (module) {
|
|
1075
|
+
const [mx, my, mw, mh] = module.layout;
|
|
1076
|
+
const scale = Math.min(boundingClientRect.width / mw, boundingClientRect.height / mh);
|
|
1077
|
+
const translateX = boundingClientRect.width / 2 - (mx + mw / 2) * scale;
|
|
1078
|
+
const translateY = boundingClientRect.height / 2 - (my + mh / 2) * scale;
|
|
1079
|
+
const initialScale = self.scaleRatio;
|
|
1080
|
+
const initialTranslateX = self.translateX;
|
|
1081
|
+
const initialTranslateY = self.translateY;
|
|
1082
|
+
const startTime = Date.now();
|
|
1083
|
+
const animationDuration = 300;
|
|
1084
|
+
const draw = ()=>{
|
|
1085
|
+
if (self.forceDestroy) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const elapsed = Date.now() - startTime;
|
|
1089
|
+
const progress = Math.min(elapsed / animationDuration, 1);
|
|
1090
|
+
const easedProgress = easing.cubicInOut(progress);
|
|
1091
|
+
const scaleRatio = initialScale + (scale - initialScale) * easedProgress;
|
|
1092
|
+
self.translateX = initialTranslateX + (translateX - initialTranslateX) * easedProgress;
|
|
1093
|
+
self.translateY = initialTranslateY + (translateY - initialTranslateY) * easedProgress;
|
|
1094
|
+
self.scaleRatio = scaleRatio;
|
|
1095
|
+
treemap.reset();
|
|
1096
|
+
applyGraphTransform(treemap.elements, self.translateX, self.translateY, scaleRatio);
|
|
1097
|
+
treemap.update();
|
|
1098
|
+
if (progress < 1) {
|
|
1099
|
+
window.requestAnimationFrame(draw);
|
|
1100
|
+
} else {
|
|
1101
|
+
self.layoutWidth = w;
|
|
1102
|
+
self.layoutHeight = h;
|
|
1103
|
+
isAnimating = false;
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
if (!isAnimating) {
|
|
1107
|
+
isAnimating = true;
|
|
1108
|
+
window.requestAnimationFrame(draw);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
root = node;
|
|
1112
|
+
}
|
|
1113
|
+
// Only works for mouseup and mousedown events
|
|
1114
|
+
function isScrollWheelOrRightButtonOnMouseupAndDown(e) {
|
|
1115
|
+
return e.which === 2 || e.which === 3;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const defaultRegistries = [
|
|
1119
|
+
registerModuleForSchedule(new SelfEvent())
|
|
1120
|
+
];
|
|
1121
|
+
function charCodeWidth(c, ch) {
|
|
1122
|
+
return c.measureText(String.fromCharCode(ch)).width;
|
|
1123
|
+
}
|
|
1124
|
+
function evaluateOptimalFontSize(c, text, width, fontRange, fontFamily, height) {
|
|
1125
|
+
height = Math.floor(height);
|
|
1126
|
+
let optimalFontSize = fontRange.min;
|
|
1127
|
+
for(let fontSize = fontRange.min; fontSize <= fontRange.max; fontSize++){
|
|
1128
|
+
c.font = `${fontSize}px ${fontFamily}`;
|
|
1129
|
+
let textWidth = 0;
|
|
1130
|
+
const textHeight = fontSize;
|
|
1131
|
+
let i = 0;
|
|
1132
|
+
while(i < text.length){
|
|
1133
|
+
const codePointWidth = charCodeWidth(c, text.charCodeAt(i));
|
|
1134
|
+
textWidth += codePointWidth;
|
|
1135
|
+
i++;
|
|
1136
|
+
}
|
|
1137
|
+
if (textWidth >= width) {
|
|
1138
|
+
const overflow = textWidth - width;
|
|
1139
|
+
const ratio = overflow / textWidth;
|
|
1140
|
+
const newFontSize = Math.abs(Math.floor(fontSize - fontSize * ratio));
|
|
1141
|
+
optimalFontSize = newFontSize || fontRange.min;
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
if (textHeight >= height) {
|
|
1145
|
+
const overflow = textHeight - height;
|
|
1146
|
+
const ratio = overflow / textHeight;
|
|
1147
|
+
const newFontSize = Math.abs(Math.floor(fontSize - fontSize * ratio));
|
|
1148
|
+
optimalFontSize = newFontSize || fontRange.min;
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
optimalFontSize = fontSize;
|
|
1152
|
+
}
|
|
1153
|
+
return optimalFontSize;
|
|
1154
|
+
}
|
|
1155
|
+
function getSafeText(c, text, width) {
|
|
1156
|
+
const ellipsisWidth = c.measureText('...').width;
|
|
1157
|
+
if (width < ellipsisWidth) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
let textWidth = 0;
|
|
1161
|
+
let i = 0;
|
|
1162
|
+
while(i < text.length){
|
|
1163
|
+
const codePointWidth = charCodeWidth(c, text.charCodeAt(i));
|
|
1164
|
+
textWidth += codePointWidth;
|
|
1165
|
+
i++;
|
|
1166
|
+
}
|
|
1167
|
+
if (textWidth < width) {
|
|
1168
|
+
return {
|
|
1169
|
+
text,
|
|
1170
|
+
width: textWidth
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
text: '...',
|
|
1175
|
+
width: ellipsisWidth
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function createFillBlock(color, x, y, width, height) {
|
|
1179
|
+
return new Rect({
|
|
1180
|
+
width,
|
|
1181
|
+
height,
|
|
1182
|
+
x,
|
|
1183
|
+
y,
|
|
1184
|
+
style: {
|
|
1185
|
+
fill: color,
|
|
1186
|
+
opacity: 1
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function createTitleText(text, x, y, font, color) {
|
|
1191
|
+
return new Text({
|
|
1192
|
+
text,
|
|
1193
|
+
x,
|
|
1194
|
+
y,
|
|
1195
|
+
style: {
|
|
1196
|
+
fill: color,
|
|
1197
|
+
textAlign: 'center',
|
|
1198
|
+
baseline: 'middle',
|
|
1199
|
+
font,
|
|
1200
|
+
lineWidth: 1
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
function resetLayout(treemap, w, h) {
|
|
1205
|
+
treemap.layoutNodes = squarify(treemap.data, {
|
|
1206
|
+
w,
|
|
1207
|
+
h,
|
|
1208
|
+
x: 0,
|
|
1209
|
+
y: 0
|
|
1210
|
+
}, treemap.decorator.layout);
|
|
1211
|
+
treemap.reset();
|
|
1212
|
+
}
|
|
1213
|
+
// https://www.typescriptlang.org/docs/handbook/mixins.html
|
|
1214
|
+
class Schedule extends etoile.Schedule {
|
|
1215
|
+
}
|
|
1216
|
+
class TreemapLayout extends Schedule {
|
|
1217
|
+
data;
|
|
1218
|
+
layoutNodes;
|
|
1219
|
+
decorator;
|
|
1220
|
+
bgBox;
|
|
1221
|
+
fgBox;
|
|
1222
|
+
constructor(...args){
|
|
1223
|
+
super(...args);
|
|
1224
|
+
this.data = [];
|
|
1225
|
+
this.layoutNodes = [];
|
|
1226
|
+
this.bgBox = new Box();
|
|
1227
|
+
this.fgBox = new Box();
|
|
1228
|
+
this.decorator = Object.create(null);
|
|
1229
|
+
}
|
|
1230
|
+
drawBackgroundNode(node) {
|
|
1231
|
+
for (const child of node.children){
|
|
1232
|
+
this.drawBackgroundNode(child);
|
|
1233
|
+
}
|
|
1234
|
+
const [x, y, w, h] = node.layout;
|
|
1235
|
+
const { rectGap, titleHeight } = node.decorator;
|
|
1236
|
+
const fill = this.decorator.color.mappings[node.node.id];
|
|
1237
|
+
if (node.children.length) {
|
|
1238
|
+
const box = new Box();
|
|
1239
|
+
box.add(createFillBlock(fill, x, y, w, titleHeight), createFillBlock(fill, x, y + h - rectGap, w, rectGap), createFillBlock(fill, x, y + titleHeight, rectGap, h - titleHeight - rectGap), createFillBlock(fill, x + w - rectGap, y + titleHeight, rectGap, h - titleHeight - rectGap));
|
|
1240
|
+
this.bgBox.add(box);
|
|
1241
|
+
} else {
|
|
1242
|
+
this.bgBox.add(createFillBlock(fill, x, y, w, h));
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
drawForegroundNode(node) {
|
|
1246
|
+
for (const child of node.children){
|
|
1247
|
+
this.drawForegroundNode(child);
|
|
1248
|
+
}
|
|
1249
|
+
const [x, y, w, h] = node.layout;
|
|
1250
|
+
const { rectBorderWidth, titleHeight, rectGap } = node.decorator;
|
|
1251
|
+
const { fontSize, fontFamily, color } = this.decorator.font;
|
|
1252
|
+
const rect = new Rect({
|
|
1253
|
+
x: x + 0.5,
|
|
1254
|
+
y: y + 0.5,
|
|
1255
|
+
width: w,
|
|
1256
|
+
height: h,
|
|
1257
|
+
style: {
|
|
1258
|
+
stroke: '#222',
|
|
1259
|
+
lineWidth: rectBorderWidth
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
this.fgBox.add(rect);
|
|
1263
|
+
this.render.ctx.textBaseline = 'middle';
|
|
1264
|
+
const optimalFontSize = evaluateOptimalFontSize(this.render.ctx, node.node.label, w - rectGap * 2, fontSize, fontFamily, node.children.length ? Math.round(titleHeight / 2) + rectGap : h);
|
|
1265
|
+
this.render.ctx.font = `${optimalFontSize}px ${fontFamily}`;
|
|
1266
|
+
if (h > titleHeight) {
|
|
1267
|
+
const result = getSafeText(this.render.ctx, node.node.label, w - rectGap * 2);
|
|
1268
|
+
if (!result) return;
|
|
1269
|
+
const { text, width } = result;
|
|
1270
|
+
const textX = x + Math.round((w - width) / 2);
|
|
1271
|
+
let textY = y + Math.round(h / 2);
|
|
1272
|
+
if (node.children.length) {
|
|
1273
|
+
textY = y + Math.round(titleHeight / 2);
|
|
1274
|
+
}
|
|
1275
|
+
this.fgBox.add(createTitleText(text, textX, textY, `${optimalFontSize}px ${fontFamily}`, color));
|
|
1276
|
+
} else {
|
|
1277
|
+
const ellipsisWidth = 3 * charCodeWidth(this.render.ctx, 46);
|
|
1278
|
+
const textX = x + Math.round((w - ellipsisWidth) / 2);
|
|
1279
|
+
const textY = y + Math.round(h / 2);
|
|
1280
|
+
this.fgBox.add(createTitleText('...', textX, textY, `${optimalFontSize}px ${fontFamily}`, color));
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
reset() {
|
|
1284
|
+
this.bgBox.destory();
|
|
1285
|
+
this.fgBox.destory();
|
|
1286
|
+
this.remove(this.bgBox, this.fgBox);
|
|
1287
|
+
for (const node of this.layoutNodes){
|
|
1288
|
+
this.drawBackgroundNode(node);
|
|
1289
|
+
this.drawForegroundNode(node);
|
|
1290
|
+
}
|
|
1291
|
+
this.add(this.bgBox, this.fgBox);
|
|
1292
|
+
}
|
|
1293
|
+
get api() {
|
|
1294
|
+
return {
|
|
1295
|
+
zoom: (node)=>{
|
|
1296
|
+
this.event.emit('zoom', node);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function createTreemap() {
|
|
1302
|
+
let treemap = null;
|
|
1303
|
+
let root = null;
|
|
1304
|
+
const uses = [];
|
|
1305
|
+
const context = {
|
|
1306
|
+
init,
|
|
1307
|
+
dispose,
|
|
1308
|
+
setOptions,
|
|
1309
|
+
resize,
|
|
1310
|
+
use
|
|
1311
|
+
};
|
|
1312
|
+
function init(el) {
|
|
1313
|
+
treemap = new TreemapLayout(el);
|
|
1314
|
+
root = el;
|
|
1315
|
+
}
|
|
1316
|
+
function dispose() {
|
|
1317
|
+
if (root && treemap) {
|
|
1318
|
+
treemap.destory();
|
|
1319
|
+
root.removeChild(root.firstChild);
|
|
1320
|
+
root = null;
|
|
1321
|
+
treemap = null;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function resize() {
|
|
1325
|
+
if (!treemap || !root) return;
|
|
1326
|
+
const { width, height } = root.getBoundingClientRect();
|
|
1327
|
+
treemap.render.initOptions({
|
|
1328
|
+
height,
|
|
1329
|
+
width,
|
|
1330
|
+
devicePixelRatio: window.devicePixelRatio
|
|
1331
|
+
});
|
|
1332
|
+
treemap.event.emit('cleanup:selfevent');
|
|
1333
|
+
resetLayout(treemap, width, height);
|
|
1334
|
+
treemap.update();
|
|
1335
|
+
}
|
|
1336
|
+
function setOptions(options) {
|
|
1337
|
+
if (!treemap) {
|
|
1338
|
+
throw new Error('Treemap not initialized');
|
|
1339
|
+
}
|
|
1340
|
+
treemap.data = bindParentForModule(options.data || []);
|
|
1341
|
+
for (const registry of defaultRegistries){
|
|
1342
|
+
registry(context, treemap, treemap.render);
|
|
1343
|
+
}
|
|
1344
|
+
for (const use of uses){
|
|
1345
|
+
use(treemap);
|
|
1346
|
+
}
|
|
1347
|
+
resize();
|
|
1348
|
+
}
|
|
1349
|
+
function use(key, register) {
|
|
1350
|
+
switch(key){
|
|
1351
|
+
case 'decorator':
|
|
1352
|
+
uses.push((treemap)=>register(treemap));
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return context;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const defaultLayoutOptions = {
|
|
1360
|
+
titleAreaHeight: {
|
|
1361
|
+
max: 80,
|
|
1362
|
+
min: 20
|
|
1363
|
+
},
|
|
1364
|
+
rectGap: 5,
|
|
1365
|
+
rectBorderRadius: 0.5,
|
|
1366
|
+
rectBorderWidth: 1.5
|
|
1367
|
+
};
|
|
1368
|
+
const defaultFontOptions = {
|
|
1369
|
+
color: '#000',
|
|
1370
|
+
fontSize: {
|
|
1371
|
+
max: 38,
|
|
1372
|
+
min: 7
|
|
1373
|
+
},
|
|
1374
|
+
fontFamily: 'sans-serif'
|
|
1375
|
+
};
|
|
1376
|
+
function presetDecorator(app) {
|
|
1377
|
+
Object.assign(app.decorator, {
|
|
1378
|
+
layout: defaultLayoutOptions,
|
|
1379
|
+
font: defaultFontOptions,
|
|
1380
|
+
color: colorMappings(app)
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
function colorDecorator(node, state) {
|
|
1384
|
+
const depth = getNodeDepth(node);
|
|
1385
|
+
let baseHue = 0;
|
|
1386
|
+
let sweepAngle = Math.PI * 2;
|
|
1387
|
+
const totalHueRange = Math.PI;
|
|
1388
|
+
if (node.parent) {
|
|
1389
|
+
sweepAngle = node.weight / node.parent.weight * sweepAngle;
|
|
1390
|
+
baseHue = state.hue + sweepAngle / Math.PI * 180;
|
|
1391
|
+
}
|
|
1392
|
+
baseHue += sweepAngle;
|
|
1393
|
+
const depthHueOffset = depth + totalHueRange / 10;
|
|
1394
|
+
const finalHue = baseHue + depthHueOffset / 2;
|
|
1395
|
+
const saturation = 0.6 + 0.4 * Math.max(0, Math.cos(finalHue));
|
|
1396
|
+
const lightness = 0.5 + 0.2 * Math.max(0, Math.cos(finalHue + Math.PI * 2 / 3));
|
|
1397
|
+
state.hue = baseHue;
|
|
1398
|
+
return {
|
|
1399
|
+
mode: 'hsl',
|
|
1400
|
+
desc: {
|
|
1401
|
+
h: finalHue,
|
|
1402
|
+
s: Math.round(saturation * 100),
|
|
1403
|
+
l: Math.round(lightness * 100)
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
function evaluateColorMappingByNode(node, state) {
|
|
1408
|
+
const colorMappings = {};
|
|
1409
|
+
if (node.groups && Array.isArray(node.groups)) {
|
|
1410
|
+
for (const child of node.groups){
|
|
1411
|
+
Object.assign(colorMappings, evaluateColorMappingByNode(child, state));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (node.id) {
|
|
1415
|
+
colorMappings[node.id] = colorDecorator(node, state);
|
|
1416
|
+
}
|
|
1417
|
+
return colorMappings;
|
|
1418
|
+
}
|
|
1419
|
+
function colorMappings(app) {
|
|
1420
|
+
const colorMappings = {};
|
|
1421
|
+
const state = {
|
|
1422
|
+
hue: 0
|
|
1423
|
+
};
|
|
1424
|
+
for (const node of app.data){
|
|
1425
|
+
Object.assign(colorMappings, evaluateColorMappingByNode(node, state));
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
mappings: colorMappings
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
exports.c2m = c2m;
|
|
1433
|
+
exports.createTreemap = createTreemap;
|
|
1434
|
+
exports.defaultFontOptions = defaultFontOptions;
|
|
1435
|
+
exports.defaultLayoutOptions = defaultLayoutOptions;
|
|
1436
|
+
exports.flattenModule = flatten;
|
|
1437
|
+
exports.presetDecorator = presetDecorator;
|
|
1438
|
+
exports.sortChildrenByKey = sortChildrenByKey;
|