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