evui 3.3.18 → 3.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,602 @@
1
+ import { convertToPercent } from '../../../common/utils';
2
+
3
+ const MAX_HANDLE_SIZE = 28;
4
+
5
+ const MIN_BOX_SIZE = {
6
+ width: 70,
7
+ height: 60,
8
+ };
9
+
10
+ const modules = {
11
+ /**
12
+ * Create legend DOM
13
+ *
14
+ * @returns {undefined}
15
+ */
16
+ createLegendLayout() {
17
+ this.legendDOM = document.createElement('div');
18
+ this.legendDOM.className = 'ev-chart-legend';
19
+ this.legendBoxDOM = document.createElement('div');
20
+ this.legendBoxDOM.className = 'ev-chart-legend-box';
21
+ this.containerDOM = document.createElement('div');
22
+ this.containerDOM.className = 'ev-chart-legend-container';
23
+
24
+ this.legendBoxDOM.appendChild(this.containerDOM);
25
+ this.legendDOM.appendChild(this.legendBoxDOM);
26
+ this.wrapperDOM.appendChild(this.legendDOM);
27
+ },
28
+
29
+ /**
30
+ * Initialize legend
31
+ * If there was no initialization, create DOM and set default layout.
32
+ * It not, there will already be set layout, so add a legend for each series with group
33
+ *
34
+ * @returns {undefined}
35
+ */
36
+ initLegend() {
37
+ if (!this.isInitLegend) {
38
+ this.createLegendLayout();
39
+ this.createLegend();
40
+ }
41
+
42
+ Object.values(this.seriesList).forEach((series) => {
43
+ this.setLegendStyle(series);
44
+ });
45
+ this.initEvent();
46
+
47
+ this.isInitLegend = true;
48
+ this.legendDragInfo = {
49
+ dragging: false,
50
+ isStart: true,
51
+ };
52
+ },
53
+
54
+ /**
55
+ * Initialize legend event
56
+ *
57
+ * @returns {undefined}
58
+ */
59
+ initEvent() {
60
+ if (this.isInitLegend) {
61
+ return;
62
+ }
63
+
64
+ this.onLegendMouseDown = (e) => {
65
+ e.stopPropagation();
66
+ e.preventDefault();
67
+
68
+ const type = e.target.dataset.type;
69
+
70
+ let targetDOM;
71
+ if (type === 'handle') {
72
+ targetDOM = e.target;
73
+ } else if (type === 'handle-btn') {
74
+ targetDOM = e.target.parentElement;
75
+ } else if (type === 'handle-btn-color') {
76
+ targetDOM = e.target.parentElement.parentElement;
77
+ } else {
78
+ return;
79
+ }
80
+
81
+ const seriesList = Object.values(this.seriesList);
82
+ if (!seriesList.length) {
83
+ return;
84
+ }
85
+
86
+ const { colorState } = seriesList[0];
87
+ const { start, end } = colorState[0];
88
+
89
+ colorState[0].selectedValue = null;
90
+ this.clearOverlay();
91
+
92
+ this.legendDragInfo.dragging = true;
93
+ this.legendDragInfo.isStart = start !== end
94
+ ? targetDOM.className.includes('start')
95
+ : this.legendDragInfo.isStart;
96
+ targetDOM.classList.add('dragging');
97
+ this.legendBoxDOM.addEventListener('mousemove', this.onLegendMouseMove, false);
98
+ this.legendBoxDOM.addEventListener('mouseup', this.onLegendMouseUp, false);
99
+ };
100
+
101
+ this.onLegendMouseMove = (e) => {
102
+ e.stopPropagation();
103
+ e.preventDefault();
104
+
105
+ const { dragging, isStart } = this.legendDragInfo;
106
+
107
+ if (dragging) {
108
+ let value = this.getSelectedValue(e);
109
+ value = this.isSide ? 100 - value : value;
110
+ const dir = isStart ? 'start' : 'end';
111
+
112
+ const seriesList = Object.values(this.seriesList);
113
+ if (!seriesList.length) {
114
+ return;
115
+ }
116
+
117
+ const { colorState } = seriesList[0];
118
+ const { start, end } = colorState[0];
119
+ if ((isStart && value > end) || (!isStart && value < start)) {
120
+ return;
121
+ }
122
+ colorState[0][dir] = value;
123
+
124
+ this.update({
125
+ updateSeries: false,
126
+ updateSelTip: { update: false, keepDomain: false },
127
+ });
128
+ }
129
+ };
130
+
131
+ this.onLegendMouseUp = () => {
132
+ this.legendDragInfo.dragging = false;
133
+
134
+ const targetDOM = this.containerDOM.getElementsByClassName('ev-chart-legend-handle dragging')[0];
135
+ targetDOM?.classList.remove('dragging');
136
+ this.legendBoxDOM.removeEventListener('mouseup', this.onLegendMouseUp, false);
137
+ };
138
+
139
+ /**
140
+ * callback for legendBoxDOM hovering
141
+ *
142
+ * @returns {undefined}
143
+ */
144
+ this.onLegendBoxOver = (e) => {
145
+ const type = e.target.dataset.type;
146
+
147
+ const seriesList = Object.values(this.seriesList);
148
+ if (!seriesList.length) {
149
+ return;
150
+ }
151
+
152
+ const { colorState, valueOpt } = seriesList[0];
153
+ const state = colorState[0];
154
+
155
+ let value = this.getSelectedValue(e);
156
+ value = this.isSide ? 100 - value : value;
157
+ if (['line', 'thumb', 'layer', 'overlay', 'overlay-item'].includes(type)) {
158
+ if (state.start <= value && value <= state.end) {
159
+ state.selectedValue = value;
160
+ this.createLegendOverlay(value, valueOpt);
161
+ } else {
162
+ return;
163
+ }
164
+ } else if (['handle', 'handle-btn', 'handle-btn-color'].includes(type)) {
165
+ const isStart = e.target.className.includes('start');
166
+ state.selectedValue = isStart ? state.start : state.end;
167
+ this.clearOverlay();
168
+ } else {
169
+ return;
170
+ }
171
+
172
+ this.update({
173
+ updateSeries: false,
174
+ updateSelTip: { update: false, keepDomain: false },
175
+ });
176
+ };
177
+
178
+ /**
179
+ * callback for mouseleave event on legendBoxDOM
180
+ *
181
+ * @returns {undefined}
182
+ */
183
+ this.onLegendBoxLeave = () => {
184
+ this.legendDragInfo.dragging = false;
185
+
186
+ const lineDOM = this.containerDOM.getElementsByClassName('ev-chart-legend-line')[0];
187
+ const targetDOM = lineDOM.getElementsByClassName('ev-chart-legend-thumb')[0];
188
+ this.clearOverlay(targetDOM);
189
+
190
+ const seriesList = Object.values(this.seriesList);
191
+ if (!seriesList.length) {
192
+ return;
193
+ }
194
+
195
+ const { colorState } = seriesList[0];
196
+ colorState[0].selectedValue = null;
197
+
198
+ this.update({
199
+ updateSeries: false,
200
+ updateSelTip: { update: false, keepDomain: false },
201
+ });
202
+ };
203
+
204
+ this.legendBoxDOM.addEventListener('mousedown', this.onLegendMouseDown);
205
+ this.legendBoxDOM.addEventListener('mouseover', this.onLegendBoxOver);
206
+ this.legendBoxDOM.addEventListener('mouseleave', this.onLegendBoxLeave);
207
+ },
208
+
209
+ getSelectedValue(evt) {
210
+ const { x, y, width, height } = this.containerDOM.getBoundingClientRect();
211
+ const isTop = !this.isSide;
212
+
213
+ const sp = isTop ? x : y;
214
+ const size = isTop ? width : height;
215
+ let movePoint = isTop ? evt.clientX : evt.clientY;
216
+ if (movePoint < sp) {
217
+ movePoint = sp;
218
+ } else if (movePoint > sp + size) {
219
+ movePoint = sp + size;
220
+ }
221
+
222
+ const move = movePoint - sp;
223
+ return +convertToPercent(move, size);
224
+ },
225
+
226
+ /**
227
+ * To update legend, reset all process.
228
+ *
229
+ * @returns {undefined}
230
+ */
231
+ updateLegend() {
232
+ this.resetLegend();
233
+ this.createLegend();
234
+
235
+ Object.values(this.seriesList).forEach((series) => {
236
+ this.setLegendStyle(series);
237
+ });
238
+ },
239
+
240
+ /**
241
+ * To update legend, remove all of legendBoxDOM's children
242
+ *
243
+ * @returns {undefined}
244
+ */
245
+ resetLegend() {
246
+ const containerDOM = this.containerDOM;
247
+
248
+ if (!containerDOM) {
249
+ return;
250
+ }
251
+
252
+ while (containerDOM.hasChildNodes()) {
253
+ containerDOM.removeChild(containerDOM.firstChild);
254
+ }
255
+ },
256
+
257
+ clearOverlay() {
258
+ const targetDOM = this.containerDOM.getElementsByClassName('ev-chart-legend-line')[0];
259
+ const overlayDOM = targetDOM.getElementsByClassName('ev-chart-legend-overlay')[0];
260
+ if (overlayDOM) {
261
+ targetDOM.removeChild(overlayDOM);
262
+
263
+ const thumbDOM = targetDOM.getElementsByClassName('ev-chart-legend-thumb')[0];
264
+ const labels = thumbDOM.children;
265
+ labels.forEach((labelDOM) => {
266
+ labelDOM.style.opacity = 1;
267
+ });
268
+ }
269
+ },
270
+
271
+ createLegendOverlay(value, opt) {
272
+ this.clearOverlay();
273
+ const handleSize = this.legendHandleSize;
274
+ const { min, max } = opt;
275
+ if (min === undefined || max === undefined) {
276
+ return;
277
+ }
278
+
279
+ const targetDOM = this.containerDOM.getElementsByClassName('ev-chart-legend-line')[0];
280
+
281
+ const overlayDOM = document.createElement('div');
282
+ overlayDOM.className = 'ev-chart-legend-overlay';
283
+ overlayDOM.dataset.type = 'overlay';
284
+
285
+ const tooltipDOM = document.createElement('div');
286
+ tooltipDOM.className = 'ev-chart-legend-overlay-tooltip';
287
+ tooltipDOM.innerText = Math.floor(min + ((max - min) * (value / 100)));
288
+
289
+ const itemDOM = document.createElement('span');
290
+ itemDOM.className = 'ev-chart-legend-overlay-item';
291
+ itemDOM.dataset.type = 'overlay-item';
292
+
293
+ let itemStyle;
294
+ let tooltipStyle;
295
+ const position = Math.floor(handleSize / 2) + 14;
296
+ if (this.isSide) {
297
+ tooltipStyle = `top:${100 - value}%;left:${position}px;transform:translateY(-50%);`;
298
+ itemStyle = `top:${100 - value}%;transform:translateY(-50%);`;
299
+ } else {
300
+ tooltipStyle = `top:-${position}px;left:${value}%;transform:translateX(-50%);`;
301
+ itemStyle = `left:${value}%;transform:translateX(-50%);`;
302
+ }
303
+ itemStyle += `width:${handleSize - 10}px;height:${handleSize - 10}px;`;
304
+
305
+ tooltipDOM.style.cssText = tooltipStyle;
306
+ itemDOM.style.cssText = itemStyle;
307
+
308
+ overlayDOM.appendChild(tooltipDOM);
309
+ overlayDOM.appendChild(itemDOM);
310
+ targetDOM.appendChild(overlayDOM);
311
+
312
+ const thumbDOM = targetDOM.getElementsByClassName('ev-chart-legend-thumb')[0];
313
+ const labels = thumbDOM.children;
314
+ labels.forEach((labelDOM) => {
315
+ labelDOM.style.opacity = 0.2;
316
+ });
317
+ },
318
+
319
+ createLegendHandle(type) {
320
+ const colorBtnDOM = document.createElement('span');
321
+ colorBtnDOM.className = `ev-chart-legend-handle-btn-color ${type}`;
322
+ colorBtnDOM.dataset.type = 'handle-btn-color';
323
+
324
+ const btnDOM = document.createElement('div');
325
+ btnDOM.className = `ev-chart-legend-handle-btn ${type}`;
326
+ btnDOM.dataset.type = 'handle-btn';
327
+
328
+ btnDOM.appendChild(colorBtnDOM);
329
+
330
+ const handleDOM = document.createElement('div');
331
+ handleDOM.className = `ev-chart-legend-handle ${type}`;
332
+ handleDOM.dataset.type = 'handle';
333
+
334
+ handleDOM.appendChild(btnDOM);
335
+
336
+ return handleDOM;
337
+ },
338
+
339
+ createLegendLabel() {
340
+ const textDOM = document.createElement('span');
341
+ textDOM.className = 'ev-chart-legend-label-text';
342
+
343
+ const labelDOM = document.createElement('div');
344
+ labelDOM.className = 'ev-chart-legend-label';
345
+
346
+ return labelDOM;
347
+ },
348
+
349
+ /**
350
+ * Create legend DOM
351
+ *
352
+ * @returns {undefined}
353
+ */
354
+ createLegend() {
355
+ if (!Object.values(this.seriesList).length) {
356
+ return;
357
+ }
358
+
359
+ const opt = this.options.legend;
360
+ this.isSide = !['top', 'bottom'].includes(opt.position);
361
+ const legendSize = this.isSide ? opt.width : opt.height;
362
+ this.legendHandleSize = legendSize > MAX_HANDLE_SIZE ? MAX_HANDLE_SIZE : legendSize;
363
+ const handleSize = this.legendHandleSize;
364
+
365
+ const startHandleDOM = this.createLegendHandle(this.isSide ? 'end' : 'start', handleSize);
366
+ const endHandleDOM = this.createLegendHandle(this.isSide ? 'start' : 'end', handleSize);
367
+
368
+ const lineLayerDOM = document.createElement('div');
369
+ lineLayerDOM.className = 'ev-chart-legend-line-layer';
370
+ lineLayerDOM.dataset.type = 'line-layer';
371
+ const thumbDOM = document.createElement('div');
372
+ thumbDOM.className = 'ev-chart-legend-thumb';
373
+ thumbDOM.dataset.type = 'thumb';
374
+
375
+ thumbDOM.appendChild(this.createLegendLabel());
376
+ thumbDOM.appendChild(this.createLegendLabel());
377
+
378
+ const lineDOM = document.createElement('div');
379
+ lineDOM.className = 'ev-chart-legend-line';
380
+ lineDOM.dataset.type = 'line';
381
+
382
+ lineDOM.appendChild(lineLayerDOM);
383
+ lineDOM.appendChild(thumbDOM);
384
+
385
+ this.containerDOM.appendChild(lineDOM);
386
+ this.containerDOM.appendChild(startHandleDOM);
387
+ this.containerDOM.appendChild(endHandleDOM);
388
+ },
389
+
390
+ setLegendStyle(series) {
391
+ const dir = this.isSide ? 'top' : 'right';
392
+ const handleSize = this.legendHandleSize;
393
+
394
+ const { valueOpt, colorState } = series;
395
+
396
+ const { min, max, decimalPoint } = valueOpt;
397
+
398
+ const { start, end } = colorState[0];
399
+ const startColor = series.getColorForGradient(start);
400
+ const endColor = series.getColorForGradient(end);
401
+ let gradient = `linear-gradient(to ${dir}, `;
402
+ gradient += `${startColor}, ${endColor})`;
403
+
404
+ const labelPosition = Math.floor(handleSize / 2) + 14;
405
+ let defaultHandleStyle = `width:${handleSize}px;height:${handleSize}px;`;
406
+ let labelStyle;
407
+ let startStyle;
408
+ let endStyle;
409
+ let thumbStyle = `background:${gradient};`;
410
+
411
+ if (this.isSide) {
412
+ defaultHandleStyle += `margin-top:-${handleSize / 2}px;`;
413
+ labelStyle = `left:${labelPosition}px;transform:translateY(-50%);top:`;
414
+ startStyle = `top:${100 - end}%;`;
415
+ endStyle = `top:${100 - start}%;`;
416
+ thumbStyle += `top:${100 - end}%;height:${end - start}%;`;
417
+ } else {
418
+ defaultHandleStyle += `margin-left:-${handleSize / 2}px;`;
419
+ labelStyle = `top:-${labelPosition}px;transform:translateX(-50%);left:`;
420
+ startStyle = `left:${start}%;`;
421
+ endStyle = `left:${end}%;`;
422
+ thumbStyle += `left:${start}%;width:${end - start}%;`;
423
+ }
424
+
425
+ const minText = (min + ((max - min) * (start / 100))).toFixed(decimalPoint);
426
+ const maxText = (min + ((max - min) * (end / 100))).toFixed(decimalPoint);
427
+
428
+ const thumbDOM = this.containerDOM.getElementsByClassName('ev-chart-legend-thumb')[0];
429
+ thumbDOM.style.cssText = thumbStyle;
430
+
431
+ const labelDOM = thumbDOM.getElementsByClassName('ev-chart-legend-label');
432
+ labelDOM[0].style.cssText = `${labelStyle}0%;`;
433
+ labelDOM[1].style.cssText = `${labelStyle}100%;`;
434
+ if (min !== undefined && max !== undefined) {
435
+ labelDOM[0].innerText = this.isSide ? maxText : minText;
436
+ labelDOM[1].innerText = this.isSide ? minText : maxText;
437
+ }
438
+
439
+ const handleDOM = this.containerDOM.getElementsByClassName('ev-chart-legend-handle');
440
+ handleDOM[0].style.cssText = defaultHandleStyle + startStyle;
441
+ handleDOM[1].style.cssText = defaultHandleStyle + endStyle;
442
+
443
+ const btnDOM = this.containerDOM.getElementsByClassName('ev-chart-legend-handle-btn-color');
444
+ btnDOM[0].style.backgroundColor = this.isSide ? endColor : startColor;
445
+ btnDOM[1].style.backgroundColor = this.isSide ? startColor : endColor;
446
+ },
447
+
448
+ /**
449
+ * Set legend components position by option
450
+ *
451
+ * @returns {undefined}
452
+ */
453
+ setLegendPosition() {
454
+ const opt = this.options;
455
+ const position = opt?.legend?.position;
456
+ const { width: minWidth, height: minHeight } = MIN_BOX_SIZE;
457
+ const handleSize = this.legendHandleSize;
458
+
459
+ const title = opt?.title?.show ? opt?.title?.height : 0;
460
+ const positionTop = title + minHeight;
461
+ const { top = 0, bottom = 0, left = 0, right = 0 } = opt?.legend?.padding ?? {};
462
+ const wrapperStyle = this.wrapperDOM.style;
463
+
464
+ if (!wrapperStyle) {
465
+ return;
466
+ }
467
+
468
+ let legendStyle;
469
+ let boxStyle;
470
+ let containerStyle;
471
+ let chartRect;
472
+
473
+ switch (position) {
474
+ case 'top':
475
+ wrapperStyle.padding = `${positionTop}px 0 0 0`;
476
+ chartRect = this.chartDOM.getBoundingClientRect();
477
+
478
+ boxStyle = `padding:${handleSize + 7}px ${right}px ${bottom}px ${left}px;`;
479
+ boxStyle += 'width:100%';
480
+ boxStyle += `height${minHeight}px;`;
481
+
482
+ legendStyle = `width:${chartRect.width}px;`;
483
+ legendStyle += `height:${minHeight}px;`;
484
+ legendStyle += `top:${title}px;`;
485
+ break;
486
+ case 'right':
487
+ wrapperStyle.padding = `${title}px ${minWidth}px 0 0`;
488
+ chartRect = this.chartDOM.getBoundingClientRect();
489
+
490
+ boxStyle = `padding:${top}px ${right}px ${bottom}px ${left}px;`;
491
+ boxStyle += `width:${minWidth}px;`;
492
+ boxStyle += 'height:100%;';
493
+ boxStyle += `max-height:${chartRect.height}px;`;
494
+
495
+ legendStyle = `width:${minWidth}px;`;
496
+ legendStyle += `height:${chartRect.height}px;`;
497
+ legendStyle += `top:${title}px;right:0px;`;
498
+ break;
499
+ case 'bottom':
500
+ wrapperStyle.padding = `${title}px 0 ${minHeight}px 0`;
501
+ chartRect = this.chartDOM.getBoundingClientRect();
502
+
503
+ boxStyle = `padding:${handleSize + 7}px ${right}px ${bottom}px ${left}px;`;
504
+ boxStyle += 'width:100%;';
505
+ boxStyle += `height:${minHeight}px;`;
506
+
507
+ legendStyle = `width:${chartRect.width}px;`;
508
+ legendStyle += `height:${minHeight}px;`;
509
+
510
+ legendStyle += 'bottom:0px;left:0px;';
511
+ break;
512
+ case 'left':
513
+ wrapperStyle.padding = `${title}px 0 0 ${minWidth}px`;
514
+ chartRect = this.chartDOM.getBoundingClientRect();
515
+
516
+ boxStyle = `padding:${top}px ${right}px ${bottom}px ${left}px;`;
517
+ boxStyle += 'display:absolute;';
518
+ boxStyle += 'bottom:0px;';
519
+ boxStyle += `width:${minWidth}px;`;
520
+ boxStyle += 'height:100%;';
521
+ boxStyle += `maxHeight:${chartRect.height}px;`;
522
+
523
+ legendStyle = `width:${minWidth}px;`;
524
+ legendStyle += `height:${chartRect.height}px;`;
525
+ legendStyle += `top:${title}px;left:0px`;
526
+ break;
527
+ default:
528
+ break;
529
+ }
530
+ if (['top', 'bottom'].includes(position)) {
531
+ const containerSize = chartRect.width / 2;
532
+
533
+ containerStyle = `left:${(chartRect.width / 2) - (containerSize / 2)}px;`;
534
+ containerStyle += `width:${containerSize}px;`;
535
+ containerStyle += `height:${handleSize}px;`;
536
+ containerStyle += 'padding:4px 0;';
537
+ containerStyle += 'margin:0 4px;';
538
+ } else {
539
+ const containerSize = chartRect.height / 2;
540
+
541
+ containerStyle = `top:${(chartRect.height / 2) - (containerSize / 2)}px;`;
542
+ containerStyle += 'left:5px;';
543
+ containerStyle += `width:${handleSize}px;`;
544
+ containerStyle += `height:${containerSize}px;`;
545
+ containerStyle += 'padding:0 4px;';
546
+ containerStyle += 'margin:4px 0;';
547
+ }
548
+
549
+ this.containerDOM.style.cssText = containerStyle;
550
+ this.legendBoxDOM.style.cssText = boxStyle;
551
+ this.legendDOM.style.cssText = legendStyle;
552
+ },
553
+
554
+ /**
555
+ * Update legend components size
556
+ *
557
+ * @returns {undefined}
558
+ */
559
+ updateLegendContainerSize() {
560
+ Object.values(this.seriesList).forEach((series) => {
561
+ this.setLegendStyle(series);
562
+ });
563
+ },
564
+
565
+ /**
566
+ * Show legend components by manipulating css
567
+ *
568
+ * @returns {undefined}
569
+ */
570
+ showLegend() {
571
+ if (this.resizeDOM) {
572
+ this.resizeDOM.style.display = 'block';
573
+ }
574
+
575
+ if (this.legendDOM) {
576
+ this.legendDOM.style.display = 'block';
577
+ }
578
+ },
579
+
580
+ /**
581
+ * Hide legend components by manipulating css
582
+ *
583
+ * @returns {undefined}
584
+ */
585
+ hideLegend() {
586
+ const opt = this.options;
587
+ const wrapperStyle = this.wrapperDOM?.style;
588
+ const legendStyle = this.legendDOM?.style;
589
+ const title = opt?.title?.show ? opt?.title?.height : 0;
590
+
591
+ if (!legendStyle || !wrapperStyle) {
592
+ return;
593
+ }
594
+
595
+ legendStyle.display = 'none';
596
+ legendStyle.width = '0';
597
+ legendStyle.height = '0';
598
+ wrapperStyle.padding = `${title}px 0 0 0`;
599
+ },
600
+ };
601
+
602
+ export default modules;