• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.shadowRoot!.querySelector('#root') as HTMLDivElement).className =
159      litChartPieCfg && litChartPieCfg.data.length > 0 ? 'bg_hasdata' : 'bg_nodata';
160    this.measure();
161    this.render();
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        this.showTip(
183          this.centerX || 0,
184          this.centerY || 0,
185          this.litChartPieConfig!.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}`
186        );
187      }
188    });
189    if (!hasHover) {
190      this.hideTip();
191    }
192    this.render();
193  }
194
195  measureInitialize(): void {
196    this.data = [];
197    this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10;
198    this.labelsEL!.textContent = '';
199  }
200
201  measure(): void {
202    if (!this.litChartPieConfig) {
203      return;
204    }
205    this.measureInitialize();
206    let pieCfg = this.litChartPieConfig!;
207    let startAngle = 0;
208    let startDegree = 0;
209    let full = Math.PI / 180; //每度
210    let fullDegree = 0; //每度
211    let sum = this.litChartPieConfig.data.reduce(
212      // @ts-ignore
213      (previousValue, currentValue) => currentValue[pieCfg.angleField] + previousValue,
214      0
215    );
216    let labelArray: string[] = [];
217    this.litChartPieConfig.data.forEach((pieItem, index) => {
218      let item: Sector = {
219        id: `id-${Utils.uuid()}`,
220        color: this.litChartPieConfig!.label.color
221          ? // @ts-ignore
222            this.litChartPieConfig!.label.color(pieItem)
223          : pieChartColors[index % pieChartColors.length],
224        obj: pieItem, // @ts-ignore
225        key: pieItem[pieCfg.colorField], // @ts-ignore
226        value: pieItem[pieCfg.angleField],
227        startAngle: startAngle, // @ts-ignore
228        endAngle: startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360),
229        startDegree: startDegree, // @ts-ignore
230        endDegree: startDegree + fullDegree + (pieItem[pieCfg.angleField] / sum) * 360,
231        ease: {
232          initVal: 0, // @ts-ignore
233          step: (startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360)) / startDegree,
234          process: true,
235        },
236      };
237      this.data.push(item); // @ts-ignore
238      startAngle += full * ((pieItem[pieCfg.angleField] / sum) * 360); // @ts-ignore
239      startDegree += fullDegree + (pieItem[pieCfg.angleField] / sum) * 360; // @ts-ignore
240      let colorFieldValue = item.obj[pieCfg.colorField];
241      if (this.config?.colorFieldTransferHandler) {
242        colorFieldValue = this.config.colorFieldTransferHandler(colorFieldValue);
243      }
244      labelArray.push(`<label class="label">
245                    <div style="display: flex;flex-direction: row;margin-left: 5px;align-items: center;overflow: hidden;text-overflow: ellipsis"
246                        id="${item.id}">
247                        <div class="tag" style="background-color: ${item.color}"></div>
248                        <span class="name">${colorFieldValue}</span>
249                    </div>
250                </label>`);
251    });
252    this.labelsEL!.innerHTML = labelArray.join('');
253  }
254
255  get config(): LitChartPieConfig | null | undefined {
256    return this.litChartPieConfig;
257  }
258
259  addCanvasOnmousemoveEvent(): void {
260    this.canvas!.onmousemove = (ev): void => {
261      let rect = this.getBoundingClientRect();
262      let x = ev.pageX - rect.left - this.centerX!;
263      let y = ev.pageY - rect.top - this.centerY!;
264      if (isPointIsCircle(0, 0, x, y, this.radius!)) {
265        let degree = this.computeDegree(x, y);
266        this.data.forEach((it) => {
267          it.hover = degree >= it.startDegree! && degree <= it.endDegree!;
268          this.updateHoverItemStatus(it); // @ts-ignore
269          it.obj.isHover = it.hover;
270          if (it.hover && this.litChartPieConfig) {
271            this.litChartPieConfig.hoverHandler?.(it.obj);
272            this.showTip(
273              ev.pageX - rect.left + 10,
274              ev.pageY - this.offsetTop - 10,
275              this.litChartPieConfig.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}`
276            );
277          }
278        });
279      } else {
280        this.hideTip();
281        this.data.forEach((it) => {
282          it.hover = false; // @ts-ignore
283          it.obj.isHover = false;
284          this.updateHoverItemStatus(it);
285        });
286        this.litChartPieConfig?.hoverHandler?.(undefined);
287      }
288      this.render();
289    };
290  }
291  connectedCallback(): void {
292    super.connectedCallback();
293    this.eleShape = this.shadowRoot!.querySelector<Element>('#shape');
294    this.pieTipEL = this.shadowRoot!.querySelector<HTMLDivElement>('#tip');
295    this.labelsEL = this.shadowRoot!.querySelector<HTMLDivElement>('#labels');
296    this.canvas = this.shadowRoot!.querySelector<HTMLCanvasElement>('#canvas');
297    this.ctx = this.canvas!.getContext('2d', { alpha: true });
298    resizeCanvas(this.canvas!);
299    this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10;
300    this.centerX = this.clientWidth / 2;
301    this.centerY = this.clientHeight / 2 - 40;
302    this.ctx?.translate(this.centerX, this.centerY);
303    this.canvas!.onmouseout = (e): void => {
304      this.hideTip();
305      this.data.forEach((it) => {
306        it.hover = false;
307        this.updateHoverItemStatus(it);
308      });
309      this.render();
310    };
311    //增加点击事件
312    this.canvas!.onclick = (ev): void => {
313      let rect = this.getBoundingClientRect();
314      let x = ev.pageX - rect.left - this.centerX!;
315      let y = ev.pageY - rect.top - this.centerY!;
316      if (isPointIsCircle(0, 0, x, y, this.radius!)) {
317        let degree = this.computeDegree(x, y);
318        this.data.forEach((it) => {
319          if (degree >= it.startDegree! && degree <= it.endDegree!) {
320            // @ts-ignore
321            this.config?.angleClick?.(it.obj);
322          }
323        });
324      }
325    };
326    this.addCanvasOnmousemoveEvent();
327    this.render();
328  }
329
330  updateHoverItemStatus(item: unknown): void {
331    // @ts-ignore
332    let label = this.shadowRoot!.querySelector(`#${item.id}`);
333    if (label) {
334      // @ts-ignore
335      (label as HTMLLabelElement).style.boxShadow = item.hover ? '0 0 5px #22ffffff' : '';
336    }
337  }
338
339  computeDegree(x: number, y: number): number {
340    let degree = (360 * Math.atan(y / x)) / (2 * Math.PI);
341    if (x >= 0 && y >= 0) {
342      degree = degree;
343    } else if (x < 0 && y >= 0) {
344      degree = 180 + degree;
345    } else if (x < 0 && y < 0) {
346      degree = 180 + degree;
347    } else {
348      degree = 270 + (90 + degree);
349    }
350    return degree;
351  }
352
353  initElements(): void {
354    new ResizeObserver((entries, observer) => {
355      entries.forEach((it) => {
356        resizeCanvas(this.canvas!);
357        this.centerX = this.clientWidth / 2;
358        this.centerY = this.clientHeight / 2 - 40;
359        this.ctx?.translate(this.centerX, this.centerY);
360        this.measure();
361        this.render();
362      });
363    }).observe(this);
364  }
365
366  handleData(): void {
367    this.textRects = [];
368    if (this.litChartPieConfig!.showChartLine) {
369      this.data.forEach((dataItem) => {
370        let text = `${dataItem.value}`;
371        let metrics = this.ctx!.measureText(text);
372        let textWidth = metrics.width;
373        let textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
374        this.ctx!.beginPath();
375        this.ctx!.strokeStyle = dataItem.color!;
376        this.ctx!.fillStyle = '#595959';
377        let deg = dataItem.startDegree! + (dataItem.endDegree! - dataItem.startDegree!) / 2;
378        let dep = 25;
379        let x1 = 0 + this.radius! * Math.cos((deg * Math.PI) / 180);
380        let y1 = 0 + this.radius! * Math.sin((deg * Math.PI) / 180);
381        let x2 = 0 + (this.radius! + 13) * Math.cos((deg * Math.PI) / 180);
382        let y2 = 0 + (this.radius! + 13) * Math.sin((deg * Math.PI) / 180);
383        let x3 = 0 + (this.radius! + dep) * Math.cos((deg * Math.PI) / 180);
384        let y3 = 0 + (this.radius! + dep) * Math.sin((deg * Math.PI) / 180);
385        this.ctx!.moveTo(x1, y1);
386        this.ctx!.lineTo(x2, y2);
387        this.ctx!.stroke();
388        let rect = this.correctRect({
389          x: x3 - textWidth / 2,
390          y: y3 + textHeight / 2,
391          w: textWidth,
392          h: textHeight,
393        });
394        this.ctx?.fillText(text, rect.x, rect.y);
395        this.ctx?.closePath();
396      });
397    }
398  }
399
400  render(ease: boolean = true): void {
401    if (!this.canvas || !this.litChartPieConfig) {
402      return;
403    }
404    if (this.radius! <= 0) {
405      return;
406    }
407    this.ctx?.clearRect(0 - this.centerX!, 0 - this.centerY!, this.clientWidth, this.clientHeight);
408    this.data.forEach((it) => {
409      this.ctx!.beginPath();
410      this.ctx!.fillStyle = it.color as string;
411      this.ctx!.strokeStyle = this.data.length > 1 ? '#fff' : (it.color as string);
412      this.ctx?.moveTo(0, 0);
413      if (it.hover) {
414        this.ctx!.lineWidth = 1;
415        this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
416      } else {
417        this.ctx!.lineWidth = 1;
418        if (ease) {
419          if (it.ease!.initVal! < it.endAngle! - it.startAngle!) {
420            it.ease!.process = true;
421            this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.startAngle! + it.ease!.initVal!, false);
422            it.ease!.initVal! += it.ease!.step!;
423          } else {
424            it.ease!.process = false;
425            this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
426          }
427        } else {
428          this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
429        }
430      }
431      this.ctx?.lineTo(0, 0);
432      this.ctx?.fill();
433      this.ctx!.stroke();
434      this.ctx?.closePath();
435    });
436    this.setData(ease);
437  }
438
439  setData(ease: boolean): void {
440    this.data
441      .filter((it) => it.hover)
442      .forEach((it) => {
443        this.ctx!.beginPath();
444        this.ctx!.fillStyle = it.color as string;
445        this.ctx!.lineWidth = 1;
446        this.ctx?.moveTo(0, 0);
447        this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false);
448        this.ctx?.lineTo(0, 0);
449        this.ctx!.strokeStyle = this.data.length > 1 ? '#000' : (it.color as string);
450        this.ctx!.stroke();
451        this.ctx?.closePath();
452      });
453    this.handleData();
454    if (this.data.filter((it) => it.ease!.process).length > 0) {
455      requestAnimationFrame(() => this.render(ease));
456    }
457  }
458
459  correctRect(pieRect: Rectangle): Rectangle {
460    if (this.textRects.length === 0) {
461      this.textRects.push(pieRect);
462      return pieRect;
463    } else {
464      let rectangles = this.textRects.filter((it) => this.intersect(it, pieRect).cross);
465      if (rectangles.length === 0) {
466        this.textRects.push(pieRect);
467        return pieRect;
468      } else {
469        let it = rectangles[0];
470        let inter = this.intersect(it, pieRect);
471        if (inter.direction === 'Right') {
472          pieRect.x += inter.crossW;
473        } else if (inter.direction === 'Bottom') {
474          pieRect.y += inter.crossH;
475        } else if (inter.direction === 'Left') {
476          pieRect.x -= inter.crossW;
477        } else if (inter.direction === 'Top') {
478          pieRect.y -= inter.crossH;
479        } else if (inter.direction === 'Right-Top') {
480          pieRect.y -= inter.crossH;
481        } else if (inter.direction === 'Right-Bottom') {
482          pieRect.y += inter.crossH;
483        } else if (inter.direction === 'Left-Top') {
484          pieRect.y -= inter.crossH;
485        } else if (inter.direction === 'Left-Bottom') {
486          pieRect.y += inter.crossH;
487        }
488        this.textRects.push(pieRect);
489        return pieRect;
490      }
491    }
492  }
493
494  intersect(
495    r1: Rectangle,
496    rect: Rectangle
497  ): {
498    cross: boolean;
499    direction: string;
500    crossW: number;
501    crossH: number;
502  } {
503    let cross: boolean;
504    let direction: string = '';
505    let crossW: number;
506    let crossH: number;
507    let maxX = r1.x + r1.w > rect.x + rect.w ? r1.x + r1.w : rect.x + rect.w;
508    let maxY = r1.y + r1.h > rect.y + rect.h ? r1.y + r1.h : rect.y + rect.h;
509    let minX = r1.x < rect.x ? r1.x : rect.x;
510    let minY = r1.y < rect.y ? r1.y : rect.y;
511    cross = maxX - minX < rect.w + r1.w && maxY - minY < r1.h + rect.h;
512    crossW = Math.abs(maxX - minX - (rect.w + r1.w));
513    crossH = Math.abs(maxY - minY - (rect.y + r1.y));
514    if (rect.x > r1.x) {
515      if (rect.y > r1.y) {
516        direction = 'Right-Bottom';
517      } else if (rect.y === r1.y) {
518        direction = 'Right';
519      } else {
520        direction = 'Right-Top';
521      }
522    } else if (rect.x < r1.x) {
523      if (rect.y > r1.y) {
524        direction = 'Left-Bottom';
525      } else if (rect.y === r1.y) {
526        direction = 'Left';
527      } else {
528        direction = 'Left-Top';
529      }
530    } else {
531      direction = this.rectSuperposition(rect, r1);
532    }
533    return {
534      cross,
535      direction,
536      crossW,
537      crossH,
538    };
539  }
540
541  rectSuperposition(rect: Rectangle, r1: Rectangle): string {
542    if (rect.y > r1.y) {
543      return 'Bottom';
544    } else if (rect.y === r1.y) {
545      return 'Right'; //superposition default right
546    } else {
547      return 'Top';
548    }
549  }
550
551  showTip(x: number, y: number, msg: string): void {
552    this.pieTipEL!.style.display = 'flex';
553    this.pieTipEL!.style.top = `${y}px`;
554    this.pieTipEL!.style.left = `${x}px`;
555    this.pieTipEL!.innerHTML = msg;
556  }
557
558  hideTip(): void {
559    this.pieTipEL!.style.display = 'none';
560  }
561
562  initHtml(): string {
563    return `
564        ${initHtmlStyle}
565        <div id="root">
566            <div id="shape" class="shape active"></div>
567            <canvas id="canvas" style="top: 0;left: 0;z-index: 21"></canvas>
568            <div id="tip"></div>
569            <div id="labels"></div>
570        </div>`;
571  }
572}
573