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