1/*
2 * Copyright (C) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { resizeCanvas } from '../helper';
17import { BaseElement, element } from '../../BaseElement';
18import { LitChartPieConfig } from './LitChartPieConfig';
19import { isPointIsCircle, pieChartColors, randomRgbColor } from './LitChartPieData';
20import { Utils } from '../../../trace/component/trace/base/Utils';
21
22interface Rectangle {
23  x: number;
24  y: number;
25  w: number;
26  h: number;
27}
28
29class Sector {
30  id?: unknown;
31  obj?: unknown;
32  key: unknown;
33  value: unknown;
34  startAngle?: number;
35  endAngle?: number;
36  startDegree?: number;
37  endDegree?: number;
38  color?: string;
39  percent?: number;
40  hover?: boolean;
41  ease?: {
42    initVal?: number;
43    step?: number;
44    process?: boolean;
45  };
46}
47
48const initHtmlStyle = `
49    <style>   
50        :host {
51            display: flex;
52            flex-direction: column;
53            overflow: hidden;
54            width: 100%;
55            height: 100%;
56        }
57        .shape.active {
58            animation: color 3.75 both;    
59        }
60        @keyframes color {
61            0% { background-color: white; }
62           100% { background-color: black; }    
63        }
64        #tip{
65            background-color: #f5f5f4;
66            border: 1px solid #fff;
67            border-radius: 5px;
68            color: #333322;
69            font-size: 8pt;
70            position: absolute;
71            display: none;
72            top: 0;
73            left: 0;
74            z-index: 99;
75            pointer-events: none;
76            user-select: none;
77            padding: 5px 10px;
78            box-shadow: 0 0 10px #22ffffff;
79        }
80        #root{
81            position:relative;
82        }
83        .bg_nodata{
84            background-repeat:no-repeat;
85            background-position:center;
86            background-image: url("img/pie_chart_no_data.png");
87        }
88        .bg_hasdata{
89            background-repeat:no-repeat;
90            background-position:center;
91        }
92        
93        #labels{
94            display: grid;
95            grid-template-columns: auto auto auto auto auto;
96            /*justify-content: center;*/
97            /*align-items: center;*/
98            width: 100%;
99            height: 25%;
100            box-sizing: border-box;
101            position: absolute;
102            bottom: 0px;
103            left: 0;
104            /*margin: 0px 10px;*/
105            padding-left: 10px;
106            padding-right: 10px;
107            pointer-events: none    ;
108        }
109        .name{
110            flex: 1;
111            font-size: 9pt;
112            overflow: hidden;
113            white-space: nowrap;
114            text-overflow: ellipsis;
115            /*color: #666;*/
116            color: var(--dark-color1,#252525);
117            pointer-events: painted;
118        }
119        .label{
120            display: flex;
121            align-items: center;
122            max-lines: 1;
123            white-space: nowrap;
124            overflow: hidden;
125            padding-right: 5px;
126        }
127        .tag{
128            display: flex;
129            align-items: center;
130            justify-content: center;
131            width: 10px;
132            height: 10px;
133            border-radius: 5px;
134            margin-right: 5px;
135        }
136        </style>
137    `;
138
139@element('lit-chart-pie')
140export class LitChartPie extends BaseElement {
141  private eleShape: Element | null | undefined;
142  private pieTipEL: HTMLDivElement | null | undefined;
143  private labelsEL: HTMLDivElement | null | undefined;
144  canvas: HTMLCanvasElement | undefined | null;
145  ctx: CanvasRenderingContext2D | undefined | null;
146  litChartPieConfig: LitChartPieConfig | null | undefined;
147  centerX: number | null | undefined;
148  centerY: number | null | undefined;
149  data: Sector[] = [];
150  radius: number | undefined;
151  private textRects: Rectangle[] = [];
152
153  set config(litChartPieCfg: LitChartPieConfig | null | undefined) {
154    if (!litChartPieCfg) {
155      return;
156    }
157    this.litChartPieConfig = litChartPieCfg;
158    this.measure();
159    this.render();
160    (this.shadowRoot!.querySelector('#root') as HTMLDivElement).className =
161    this.data.length > 0 ? 'bg_hasdata' : 'bg_nodata';
162  }
163
164  set dataSource(litChartPieArr: unknown[]) {
165    if (this.litChartPieConfig) {
166      this.litChartPieConfig.data = litChartPieArr;
167      this.measure();
168      this.render();
169    }
170  }
171
172  showHover(): void {
173    let hasHover = false;
174    this.data.forEach((it) => {
175      // @ts-ignore
176      it.hover = it.obj.isHover;
177      if (it.hover) {
178        hasHover = true;
179      }
180      this.updateHoverItemStatus(it);
181      if (it.hover) {
182        if (this.centerX && this.centerX > 0 && this.centerY && this.centerY > 0) { 
183          this.showTip(
184            this.centerX - 40 || 0,
185            this.centerY || 0,
186            this.litChartPieConfig!.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}`
187          );
188        }
189      }
190    });
191    if (!hasHover) {
192      this.hideTip();
193    }
194    this.render();
195  }
196
197  measureInitialize(): void {
198    this.data = [];
199    this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10;
200    this.labelsEL!.textContent = '';
201  }
202
203  measure(): void {
204    if (!this.litChartPieConfig) {
205      return;
206    }
207    this.measureInitialize();
208    let pieCfg = this.litChartPieConfig!;
209    let startAngle = 0;
210    let startDegree = 0;
211    let full = Math.PI / 180; //每度
212    let fullDegree = 0; //每度
213    let sum = this.litChartPieConfig.data.reduce(
214      // @ts-ignore
215      (previousValue, currentValue) => currentValue[pieCfg.angleField] + previousValue,
216      0
217    );
218    let labelArray: string[] = [];
219    sum && this.litChartPieConfig.data.forEach((pieItem, index) => {
220      let item: Sector = {
221        id: `id-${Utils.uuid()}`,
222        color: this.litChartPieConfig!.label.color
223          ? // @ts-ignore
224            this.litChartPieConfig!.label.color(pieItem)
225          : pieChartColors[index % pieChartColors.length],
226        obj: pieItem, // @ts-ignore
227        key: pieItem[pieCfg.colorField], // @ts-ignore
228        value: pieItem[pieCfg.angleField],
229        startAngle: startAngle, // @ts-ignore
230        endAngle: startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360),
231        startDegree: startDegree, // @ts-ignore
232        endDegree: startDegree + fullDegree + (pieItem[pieCfg.angleField] / sum) * 360,
233        ease: {
234          initVal: 0, // @ts-ignore
235          step: (startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360)) / startDegree,
236          process: true,
237        },
238      };
239      this.data.push(item); // @ts-ignore
240      startAngle += full * ((pieItem[pieCfg.angleField] / sum) * 360); // @ts-ignore
241      startDegree += fullDegree + (pieItem[pieCfg.angleField] / sum) * 360; // @ts-ignore
242      let colorFieldValue = item.obj[pieCfg.colorField];
243      if (this.config?.colorFieldTransferHandler) {
244        colorFieldValue = this.config.colorFieldTransferHandler(colorFieldValue);
245      }
246      labelArray.push(`<label class="label">
247                    <div style="display: flex;flex-direction: row;margin-left: 5px;align-items: center;overflow: hidden;text-overflow: ellipsis" 
248                        id="${item.id}">
249                        <div class="tag" style="background-color: ${item.color}"></div>
250                        <span class="name">${colorFieldValue}</span>
251                    </div>
252                </label>`);
253    });
254    this.labelsEL!.innerHTML = labelArray.join('');
255  }
256
257  get config(): LitChartPieConfig | null | undefined {
258    return this.litChartPieConfig;
259  }
260
261  addCanvasOnmousemoveEvent(): void {
262    this.canvas!.onmousemove = (ev): void => {
263      let rect = this.getBoundingClientRect();
264      let x = ev.pageX - rect.left - this.centerX!;
265      let y = ev.pageY - rect.top - this.centerY!;
266      if (isPointIsCircle(0, 0, x, y, this.radius!)) {
267        let degree = this.computeDegree(x, y);
268        this.data.forEach((it) => {
269          it.hover = degree >= it.startDegree! && degree <= it.endDegree!;
270          this.updateHoverItemStatus(it); // @ts-ignore
271          it.obj.isHover = it.hover;
272          if (it.hover && this.litChartPieConfig) {
273            this.litChartPieConfig.hoverHandler?.(it.obj);
274            this.showTip(
275              ev.pageX - rect.left > this.centerX! ? ev.pageX - rect.left - 165 : ev.pageX - rect.left + 10,
276              ev.pageY - this.offsetTop > this.centerY! ? ev.pageY - this.offsetTop - 50 : ev.pageY + (this.offsetTop - rect.top) - this.offsetTop + 20,
277              this.litChartPieConfig.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}`
278            );
279          }
280        });
281      } else {
282        this.hideTip();
283        this.data.forEach((it) => {
284          it.hover = false; // @ts-ignore
285          it.obj.isHover = false;
286          this.updateHoverItemStatus(it);
287        });
288        this.litChartPieConfig?.hoverHandler?.(undefined);
289      }
290      this.render();
291    };
292  }
293  connectedCallback(): void {
294    super.connectedCallback();
295    this.eleShape = this.shadowRoot!.querySelector<Element>('#shape');
296    this.pieTipEL = this.shadowRoot!.querySelector<HTMLDivElement>('#tip');
297    this.labelsEL = this.shadowRoot!.querySelector<HTMLDivElement>('#labels');
298    this.canvas = this.shadowRoot!.querySelector<HTMLCanvasElement>('#canvas');
299    this.ctx = this.canvas!.getContext('2d', { alpha: true });
300    resizeCanvas(this.canvas!);
301    this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10;
302    this.centerX = this.clientWidth / 2;
303    this.centerY = this.clientHeight / 2 - 40;
304    this.ctx?.translate(this.centerX, this.centerY);
305    this.canvas!.onmouseout = (e): void => {
306      this.hideTip();
307      this.data.forEach((it) => {
308        it.hover = false;
309        this.updateHoverItemStatus(it);
310      });
311      this.render();
312    };
313    //增加点击事件
314    this.canvas!.onclick = (ev): void => {
315      let rect = this.getBoundingClientRect();
316      let x = ev.pageX - rect.left - this.centerX!;
317      let y = ev.pageY - rect.top - this.centerY!;
318      if (isPointIsCircle(0, 0, x, y, this.radius!)) {
319        let degree = this.computeDegree(x, y);
320        this.data.forEach((it) => {
321          if (degree >= it.startDegree! && degree <= it.endDegree!) {
322            // @ts-ignore
323            this.config?.angleClick?.(it.obj);
324          }
325        });
326      }
327    };
328    this.addCanvasOnmousemoveEvent();
329    this.render();
330  }
331
332  updateHoverItemStatus(item: unknown): void {
333    // @ts-ignore
334    let label = this.shadowRoot!.querySelector(`#${item.id}`);
335    if (label) {
336      // @ts-ignore
337      (label as HTMLLabelElement).style.boxShadow = item.hover ? '0 0 5px #22ffffff' : '';
338    }
339  }
340
341  computeDegree(x: number, y: number): number {
342    let degree = (360 * Math.atan(y / x)) / (2 * Math.PI);
343    if (x >= 0 && y >= 0) {
344      degree = degree;
345    } else if (x < 0 && y >= 0) {
346      degree = 180 + degree;
347    } else if (x < 0 && y < 0) {
348      degree = 180 + degree;
349    } else {
350      degree = 270 + (90 + degree);
351    }
352    return degree;
353  }
354
355  initElements(): void {
356    new ResizeObserver((entries, observer) => {
357      entries.forEach((it) => {
358        resizeCanvas(this.canvas!);
359        this.centerX = this.clientWidth / 2;
360        this.centerY = this.clientHeight / 2 - 40;
361        this.ctx?.translate(this.centerX, this.centerY);
362        this.measure();
363        this.render();
364      });
365    }).observe(this);
366  }
367
368  handleData(): void {
369    this.textRects = [];
370    if (this.litChartPieConfig!.showChartLine) {
371      this.data.forEach((dataItem) => {
372        let text = `${dataItem.value}`;
373        let metrics = this.ctx!.measureText(text);
374        let textWidth = metrics.width;
375        let textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
376        this.ctx!.beginPath();
377        this.ctx!.strokeStyle = dataItem.color!;
378        this.ctx!.fillStyle = '#595959';
379        let deg = dataItem.startDegree! + (dataItem.endDegree! - dataItem.startDegree!) / 2;
380        let dep = 25;
381        let x1 = 0 + this.radius! * Math.cos((deg * Math.PI) / 180);
382        let y1 = 0 + this.radius! * Math.sin((deg * Math.PI) / 180);
383        let x2 = 0 + (this.radius! + 13) * Math.cos((deg * Math.PI) / 180);
384        let y2 = 0 + (this.radius! + 13) * Math.sin((deg * Math.PI) / 180);
385        let x3 = 0 + (this.radius! + dep) * Math.cos((deg * Math.PI) / 180);
386        let y3 = 0 + (this.radius! + dep) * Math.sin((deg * Math.PI) / 180);
387        this.ctx!.moveTo(x1, y1);
388        this.ctx!.lineTo(x2, y2);
389        this.ctx!.stroke();
390        let rect = this.correctRect({
391          x: x3 - textWidth / 2,
392          y: y3 + textHeight / 2,
393          w: textWidth,
394          h: textHeight,
395        });
396        this.ctx?.fillText(text, rect.x, rect.y);
397        this.ctx?.closePath();
398      });
399    }
400  }
401
402  render(ease: boolean = true): void {
403    if (!this.canvas || !this.litChartPieConfig) {
404      return;
405    }
406    if (this.radius! <= 0) {
407      return;
408    }
409    this.ctx?.clearRect(0 - this.centerX!, 0 - this.centerY!, this.clientWidth, this.clientHeight);
410    this.data.forEach((it) => {
411      this.ctx!.beginPath();
412      this.ctx!.fillStyle = it.color as string;
413      this.ctx!.strokeStyle = this.data.length > 1 ? '#fff' : (it.color as string);
414      this.ctx?.moveTo(0, 0);
415      if (it.hover) {
416        this.ctx!.lineWidth = 1;
417        this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
418      } else {
419        this.ctx!.lineWidth = 1;
420        if (ease) {
421          if (it.ease!.initVal! < it.endAngle! - it.startAngle!) {
422            it.ease!.process = true;
423            this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.startAngle! + it.ease!.initVal!, false);
424            it.ease!.initVal! += it.ease!.step!;
425          } else {
426            it.ease!.process = false;
427            this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
428          }
429        } else {
430          this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
431        }
432      }
433      this.ctx?.lineTo(0, 0);
434      this.ctx?.fill();
435      this.ctx!.stroke();
436      this.ctx?.closePath();
437    });
438    this.setData(ease);
439  }
440
441  setData(ease: boolean): void {
442    this.data
443      .filter((it) => it.hover)
444      .forEach((it) => {
445        this.ctx!.beginPath();
446        this.ctx!.fillStyle = it.color as string;
447        this.ctx!.lineWidth = 1;
448        this.ctx?.moveTo(0, 0);
449        this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
450        this.ctx?.lineTo(0, 0);
451        this.ctx!.strokeStyle = this.data.length > 1 ? '#000' : (it.color as string);
452        this.ctx!.stroke();
453        this.ctx?.closePath();
454      });
455    this.handleData();
456    if (this.data.filter((it) => it.ease!.process).length > 0) {
457      requestAnimationFrame(() => this.render(ease));
458    }
459  }
460
461  correctRect(pieRect: Rectangle): Rectangle {
462    if (this.textRects.length === 0) {
463      this.textRects.push(pieRect);
464      return pieRect;
465    } else {
466      let rectangles = this.textRects.filter((it) => this.intersect(it, pieRect).cross);
467      if (rectangles.length === 0) {
468        this.textRects.push(pieRect);
469        return pieRect;
470      } else {
471        let it = rectangles[0];
472        let inter = this.intersect(it, pieRect);
473        if (inter.direction === 'Right') {
474          pieRect.x += inter.crossW;
475        } else if (inter.direction === 'Bottom') {
476          pieRect.y += inter.crossH;
477        } else if (inter.direction === 'Left') {
478          pieRect.x -= inter.crossW;
479        } else if (inter.direction === 'Top') {
480          pieRect.y -= inter.crossH;
481        } else if (inter.direction === 'Right-Top') {
482          pieRect.y -= inter.crossH;
483        } else if (inter.direction === 'Right-Bottom') {
484          pieRect.y += inter.crossH;
485        } else if (inter.direction === 'Left-Top') {
486          pieRect.y -= inter.crossH;
487        } else if (inter.direction === 'Left-Bottom') {
488          pieRect.y += inter.crossH;
489        }
490        this.textRects.push(pieRect);
491        return pieRect;
492      }
493    }
494  }
495
496  intersect(
497    r1: Rectangle,
498    rect: Rectangle
499  ): {
500    cross: boolean;
501    direction: string;
502    crossW: number;
503    crossH: number;
504  } {
505    let cross: boolean;
506    let direction: string = '';
507    let crossW: number;
508    let crossH: number;
509    let maxX = r1.x + r1.w > rect.x + rect.w ? r1.x + r1.w : rect.x + rect.w;
510    let maxY = r1.y + r1.h > rect.y + rect.h ? r1.y + r1.h : rect.y + rect.h;
511    let minX = r1.x < rect.x ? r1.x : rect.x;
512    let minY = r1.y < rect.y ? r1.y : rect.y;
513    cross = maxX - minX < rect.w + r1.w && maxY - minY < r1.h + rect.h;
514    crossW = Math.abs(maxX - minX - (rect.w + r1.w));
515    crossH = Math.abs(maxY - minY - (rect.y + r1.y));
516    if (rect.x > r1.x) {
517      if (rect.y > r1.y) {
518        direction = 'Right-Bottom';
519      } else if (rect.y === r1.y) {
520        direction = 'Right';
521      } else {
522        direction = 'Right-Top';
523      }
524    } else if (rect.x < r1.x) {
525      if (rect.y > r1.y) {
526        direction = 'Left-Bottom';
527      } else if (rect.y === r1.y) {
528        direction = 'Left';
529      } else {
530        direction = 'Left-Top';
531      }
532    } else {
533      direction = this.rectSuperposition(rect, r1);
534    }
535    return {
536      cross,
537      direction,
538      crossW,
539      crossH,
540    };
541  }
542
543  rectSuperposition(rect: Rectangle, r1: Rectangle): string {
544    if (rect.y > r1.y) {
545      return 'Bottom';
546    } else if (rect.y === r1.y) {
547      return 'Right'; //superposition default right
548    } else {
549      return 'Top';
550    }
551  }
552
553  showTip(x: number, y: number, msg: string): void {
554    this.pieTipEL!.style.display = 'flex';
555    this.pieTipEL!.style.top = `${y}px`;
556    this.pieTipEL!.style.left = `${x}px`;
557    this.pieTipEL!.innerHTML = msg;
558  }
559
560  hideTip(): void {
561    this.pieTipEL!.style.display = 'none';
562  }
563
564  initHtml(): string {
565    return `
566        ${initHtmlStyle}
567        <div id="root">
568            <div id="shape" class="shape active"></div>
569            <canvas id="canvas" style="top: 0;left: 0;z-index: 21"></canvas>
570            <div id="tip"></div>
571            <div id="labels"></div>
572        </div>`;
573  }
574}
575