1/*
2 * Copyright (C) 2023 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 { LitChartScatterConfig } from './LitChartScatterConfig';
19
20@element('lit-chart-scatter')
21export class LitChartScatter extends BaseElement {
22  private scatterTipEL: HTMLDivElement | null | undefined;
23  private labelsEL: HTMLDivElement | null | undefined;
24  canvas: HTMLCanvasElement | undefined | null;
25  canvas2: HTMLCanvasElement | undefined | null;
26  ctx: CanvasRenderingContext2D | undefined | null;
27  originX: number = 0;
28  finalX: number = 0;
29  originY: number = 0;
30  finalY: number = 0;
31  options: LitChartScatterConfig | undefined;
32
33  set config(LitChartScatterConfig: LitChartScatterConfig) {
34    this.options = LitChartScatterConfig;
35    this.init();
36  }
37  init(): void {
38    if (this.options) {
39      // 清楚上一次绘制的数据
40      this.ctx?.clearRect(0, 0, this.clientWidth, this.clientHeight);
41      this.drawBackground();
42      this.drawScatterChart(this.options);
43      //使用off-screen-canvas保存绘制的像素点
44      this.setOffScreen();
45      this.labelsEL!.innerText = this.options.title;
46    }
47  }
48  // 使用离屏技术保存绘制的像素点
49  setOffScreen(): void {
50    this.canvas2 = document.createElement('canvas');
51    this.canvas2.height = this.clientHeight;
52    this.canvas2.width = this.clientWidth;
53    let context2: CanvasRenderingContext2D | null = this.canvas2.getContext('2d');
54    if (this.canvas?.width !== 0 && this.canvas?.height !== 0) {
55      context2!.drawImage(this.canvas!, 0, 0);
56    }
57  }
58  /*绘制渐变色背景*/
59  drawBackground(): void {
60    let w: number = this.clientWidth;
61    let h: number = this.clientHeight;
62    let color: CanvasGradient = this.ctx?.createRadialGradient(w / 2, h / 2, 0.2 * w, w / 2, h / 2, 0.5 * w)!;
63    color?.addColorStop(0, '#eaeaea');
64    color?.addColorStop(1, '#ccc');
65    if (this.options) {
66      this.options!.globalGradient = color;
67    }
68    this.ctx?.save();
69    this.ctx!.fillStyle = color;
70    this.ctx?.fillRect(0, 0, w, h);
71    this.ctx?.restore();
72  }
73  /**
74   * 绘制散点图
75   */
76  drawScatterChart(options: LitChartScatterConfig): void {
77    this.drawAxis(options); //绘制坐标轴
78    this.drawYLabels(options); //绘制y轴坐标
79    this.drawXLabels(options); //绘制x轴坐标
80    let drawload: boolean = false;
81    if (options) {
82      drawload = options.drawload;
83    }
84    if (drawload) {
85      let load: Array<number> = [];
86      if (options) {
87        load = options.load;
88        this.drawBalanceLine(load); //绘制均衡线
89        this.drawLoadLine(load); //绘制最大负载线
90      }
91    }
92    this.drawData(options); //绘制散点图
93  }
94  /**
95   * 绘制坐标轴
96   */
97  drawAxis(options: LitChartScatterConfig): void {
98    let text: Array<string> = new Array();
99    if (options) {
100      text = options.axisLabel;
101    }
102    this.ctx!.font = '10px KATTI';
103    this.ctx!.fillStyle = '#000000';
104    this.ctx!.strokeStyle = '#000000';
105    // 画x轴
106    this.ctx?.beginPath();
107    this.ctx?.moveTo(this.originX, this.originY);
108    this.ctx?.lineTo(this.finalX, this.originY);
109    this.ctx?.fillText(text[0], this.finalX, this.originY);
110    this.ctx?.stroke();
111    // 画Y轴
112    this.ctx?.beginPath();
113    this.ctx?.moveTo(this.originX, this.originY);
114    this.ctx?.lineTo(this.originX, this.finalY);
115    this.ctx?.fillText(text[1], this.originX - 20, this.finalY - 10);
116    this.ctx?.stroke();
117  }
118  /**
119   * 绘制y轴坐标
120   */
121  drawYLabels(options: LitChartScatterConfig): void {
122    const AXAIS_DELTA: number = 5;
123    const QUYU: number = 100;
124    // 添加原点刻度
125    this.ctx!.font = '12px KATTI';
126    this.ctx!.fillStyle = '#000000';
127    this.ctx!.strokeStyle = '#000000';
128    this.ctx?.fillText('0', this.originX - AXAIS_DELTA, this.originY + AXAIS_DELTA * 2);
129    let yAxis: Array<number> = [];
130    if (options) {
131      yAxis = options.yAxisLabel;
132    }
133    // 画Y轴坐标尺
134    for (let i = 0; i < yAxis.length; i++) {
135      let length1: number =
136        (this.originY - this.finalY - ((this.originY - this.finalY) % QUYU)) * (yAxis[i] / yAxis[yAxis.length - 1]);
137      let length2: number = this.originY - length1;
138      let text: string = yAxis[i].toString();
139      let x: number = this.originX - this.ctx?.measureText(text).width! - AXAIS_DELTA;
140      this.ctx?.beginPath();
141      this.ctx?.moveTo(this.originX, length2);
142      this.ctx?.lineTo(this.originX + AXAIS_DELTA, length2);
143      this.ctx?.fillText(text, x, length2 + AXAIS_DELTA);
144      this.ctx?.stroke();
145    }
146  }
147  /**
148   * 绘制x轴坐标
149   */
150  drawXLabels(options: LitChartScatterConfig): void {
151    // 画X轴坐标尺
152    this.ctx!.fillStyle = '#000000';
153    this.ctx!.strokeStyle = '#000000';
154    const QUYU: number = 100;
155    const DELTA: number = 5;
156    let xAxis: Array<number> = [];
157    if (options) {
158      xAxis = options.xAxisLabel;
159    }
160    for (let i = 0; i < xAxis.length; i++) {
161      let length3: number =
162        (this.finalX - this.originX - ((this.finalX - this.originX) % QUYU)) * (xAxis[i] / xAxis[xAxis.length - 1]);
163      let length4: number = this.originX + length3;
164      this.ctx?.beginPath();
165      this.ctx?.moveTo(length4, this.originY);
166      this.ctx?.lineTo(length4, this.originY - DELTA);
167      this.ctx?.fillText(xAxis[i].toString(), length4 - DELTA * 3, this.originY + DELTA * 2);
168      this.ctx?.stroke();
169    }
170  }
171
172  /**
173   * 绘制数据
174   */
175  drawData(options: LitChartScatterConfig): void {
176    let data: Array<Array<Array<number>>> = [];
177    let yAxis: Array<number> = [];
178    let xAxis: Array<number> = [];
179    let colorPool: Array<string> = new Array();
180    let colorPoolText: Array<string> = new Array();
181    let rectY: number = this.clientHeight * 0.05;
182    const QUYU: number = 100;
183    const WIDTH_DELTA: number = 70;
184    if (options) {
185      data = options.data;
186      yAxis = options.yAxisLabel;
187      xAxis = options.xAxisLabel;
188      colorPool = options.colorPool();
189      colorPoolText = options.colorPoolText();
190      options.paintingData = [];
191    }
192    let xLength: number = this.finalX - this.originX - ((this.finalX - this.originX) % QUYU);
193    let yLength: number = this.originY - this.finalY - ((this.originY - this.finalY) % QUYU);
194    for (let i = 0; i < data.length; i++) {
195      for (let j = 0; j < data[i].length; j++) {
196        // 打点x坐标
197        let x: number = this.originX + (data[i][j][0] / xAxis[xAxis.length - 1]) * xLength;
198        // 打点y坐标
199        let y: number = this.originY - (data[i][j][1] / yAxis[yAxis.length - 1]) * yLength;
200        let r: number = 6;
201        if (i > 0) {
202          options.paintingData[data[i][j][2] - 1] = {
203            x,
204            y,
205            r,
206            c: data[i][j],
207            color: colorPool[i],
208          };
209        } else {
210          options.paintingData.push({
211            x,
212            y,
213            r,
214            c: data[i][j],
215            color: colorPool[i],
216          });
217        }
218        this.drawCycle(x, y, r, 0.8, colorPool[i]);
219      }
220      if (data[i].length) {
221        rectY = rectY + 20;
222        this.ctx?.fillText(colorPoolText[i] + ': ', this.clientWidth - WIDTH_DELTA, rectY + 4);
223        this.drawCycle(this.clientWidth - QUYU / 5, rectY, 7.5, 0.8, colorPool[i]);
224      }
225    }
226  }
227  /**
228   * 画圆点
229   */
230  drawCycle(x: number, y: number, r: number, transparency: number, color: string): void {
231    this.ctx!.fillStyle = color;
232    this.ctx?.beginPath();
233    this.ctx!.globalAlpha = transparency;
234    this.ctx?.arc(x, y, r, 0, Math.PI * 2, true);
235    this.ctx?.closePath();
236    this.ctx?.fill();
237  }
238
239  /**
240   * 绘制最大负载线
241   */
242  drawLoadLine(data: Array<number>): void {
243    let maxXAxis: number = 1;
244    const QUYU: number = 100;
245    const FOR_VALUE = 60;
246    if (this.options) {
247      maxXAxis = this.options.xAxisLabel[this.options.xAxisLabel.length - 1];
248    }
249    // data[1]用来标注n Hz负载线
250    let addr1: number =
251      this.originX + (this.finalX - this.originX - ((this.finalX - this.originX) % QUYU)) * (data[0] / maxXAxis);
252    let addr2: number = (this.originY - this.finalY - ((this.originY - this.finalY) % QUYU)) / FOR_VALUE;
253    let y: number = this.originY;
254    this.ctx!.strokeStyle = '#ff0000';
255    for (let i = 0; i < FOR_VALUE; i++) {
256      this.ctx?.beginPath();
257      this.ctx?.moveTo(addr1, y);
258      y -= addr2;
259      this.ctx?.lineTo(addr1, y);
260      if (i % 2 !== 0) {
261        this.ctx?.stroke();
262      }
263    }
264    this.ctx!.font = '10px KATTI';
265    this.ctx!.fillStyle = '#ff0000';
266    this.ctx?.fillText(
267      data[1] + 'Hz最大负载线',
268      addr1 - FOR_VALUE / 3,
269      this.originY - addr2 * FOR_VALUE - FOR_VALUE / 4
270    );
271    this.ctx!.fillStyle = '#000000';
272    this.ctx?.fillText('过供给区', addr1 / 2, y + FOR_VALUE / 2);
273    this.ctx?.fillText('欠供给区', addr1 / 2, this.originY - this.finalY);
274    this.ctx?.fillText('超负载区', addr1 + FOR_VALUE / 3, (this.finalY + this.originY) / 2);
275  }
276
277  /**
278   * 绘制均衡线
279   */
280  drawBalanceLine(data: Array<number>): void {
281    let maxXAxis: number = 1;
282    const QUYU: number = 100;
283    const FOR_VALUE = 60;
284    if (this.options) {
285      maxXAxis = this.options.xAxisLabel[this.options.xAxisLabel.length - 1];
286    }
287    // data[1]用来标注n Hz均衡线
288    let addr1: number =
289      ((this.finalX - this.originX - ((this.finalX - this.originX) % QUYU)) * (data[0] / maxXAxis)) / FOR_VALUE;
290    let addr2: number = (this.originY - this.finalY - ((this.originY - this.finalY) % QUYU)) / FOR_VALUE;
291    let x: number = this.originX;
292    let y: number = this.originY;
293    this.ctx!.strokeStyle = '#00ff00';
294    for (let i = 0; i < FOR_VALUE; i++) {
295      this.ctx?.beginPath();
296      this.ctx?.moveTo(x, y);
297      x += addr1;
298      y -= addr2;
299      this.ctx?.lineTo(x, y);
300      if (i % 2 === 0) {
301        this.ctx?.stroke();
302      }
303    }
304    this.ctx?.save();
305    this.ctx?.translate(addr1 * 25 + this.originX, addr2 * 40 + this.finalY);
306    this.ctx!.font = '10px KATTI';
307    this.ctx!.fillStyle = '#ff0f00';
308    this.ctx?.rotate(-Math.atan(addr2 / addr1));
309    this.ctx?.fillText(data[1] + 'Hz均衡线', 0, 0);
310    this.ctx?.restore();
311  }
312
313  /*检测是否hover在散点之上*/
314  checkHover(options: LitChartScatterConfig | undefined, pos: Object): Object | boolean {
315    let data: Array<Object> = [];
316    if (options) {
317      data = options.paintingData;
318    }
319    let found: boolean | Object = false;
320    for (let i = 0; i < data.length; i++) {
321      found = false;
322      // @ts-ignore
323      if (
324        Math.sqrt(
325          // @ts-ignore
326          Math.pow(pos.x - data[i].x, 2) + Math.pow(pos.y - data[i].y, 2)
327          // @ts-ignore
328        ) < data[i].r
329      ) {
330        found = data[i];
331        break;
332      }
333    }
334    return found;
335  }
336
337  /*绘制hover状态*/
338  paintHover(): void {
339    let obj: Object | null = this.options!.hoverData;
340    // @ts-ignore
341    let x: number = obj?.x;
342    // @ts-ignore
343    let y: number = obj?.y;
344    // @ts-ignore
345    let r: number = obj?.r;
346    // @ts-ignore
347    let c: string = obj?.color;
348    let step: number = 0.5;
349    this.ctx!.globalAlpha = 1;
350    this.ctx!.fillStyle = c;
351    for (let i = 0; i < 10; i++) {
352      this.ctx?.beginPath();
353      this.ctx?.arc(x, y, r + i * step, 0, 2 * Math.PI, false);
354      this.ctx?.fill();
355      this.ctx?.closePath();
356    }
357  }
358  //利用离屏canvas恢复hover前的状态
359  resetHoverWithOffScreen(): void {
360    let obj: Object | null = null;
361    const STEP_VALUE: number = 12;
362    const OUT_CYCLE: number = 2;
363    if (this.options) {
364      obj = this.options.hoverData;
365    }
366    if (!obj) {
367      return;
368    }
369    // @ts-ignore
370    let { x, y, r, c, color } = obj;
371    let step = 0.5;
372    this.ctx!.globalAlpha = 1;
373    for (let i = 10; i > 0; i--) {
374      this.ctx?.save();
375      //绘制外圆范围
376      this.ctx?.drawImage(
377        this.canvas2!,
378        x - r - STEP_VALUE * step,
379        y - r - STEP_VALUE * step,
380        OUT_CYCLE * (r + STEP_VALUE * step),
381        OUT_CYCLE * (r + STEP_VALUE * step),
382        x - r - STEP_VALUE * step,
383        y - r - STEP_VALUE * step,
384        OUT_CYCLE * (r + STEP_VALUE * step),
385        OUT_CYCLE * (r + STEP_VALUE * step)
386      );
387      //绘制内圆
388      this.ctx?.beginPath();
389      this.ctx?.arc(x, y, r + i * step, 0, OUT_CYCLE * Math.PI, false);
390      this.ctx?.closePath();
391      this.ctx!.fillStyle = color;
392      this.ctx!.globalAlpha = 0.8;
393      //填充内圆
394      this.ctx?.fill();
395      this.ctx?.restore();
396    }
397    this.options!.hoverData = null;
398  }
399  /**
400   * 显示提示框
401   */
402  showTip(data: any): void {
403    const minWidth: number = 160;
404    const miniHeight: number = 70;
405    const canvasWidth: number = Number(this.canvas?.style.width.replace('px', ''));
406    const canvasHeight: number = Number(this.canvas?.style.height.replace('px', ''));
407    this.scatterTipEL!.style.display = 'flex';
408    if (canvasWidth - data.x < minWidth && canvasHeight - data.y >= miniHeight) {
409      this.scatterTipEL!.style.top = `${data.y}px`;
410      this.scatterTipEL!.style.left = `${data.x - minWidth}px`;
411    } else if (canvasHeight - data.y < miniHeight && canvasWidth - data.x > minWidth) {
412      this.scatterTipEL!.style.top = `${data.y - miniHeight}px`;
413      this.scatterTipEL!.style.left = `${data.x}px`;
414    } else if (canvasWidth - data.x < minWidth && canvasHeight - data.y < miniHeight) {
415      this.scatterTipEL!.style.top = `${data.y - miniHeight}px`;
416      this.scatterTipEL!.style.left = `${data.x - minWidth}px`;
417    } else {
418      this.scatterTipEL!.style.top = `${data.y}px`;
419      this.scatterTipEL!.style.left = `${data.x}px`;
420    }
421    this.scatterTipEL!.innerHTML = this.options!.tip(data);
422    // @ts-ignore
423    this.options!.hoverEvent('CPU-FREQ', true, data.c[2] - 1);
424  }
425  /**
426   * 隐藏提示框
427   */
428  hideTip(): void {
429    this.scatterTipEL!.style.display = 'none';
430    if (this.options) {
431      // @ts-ignore
432      this.options!.hoverEvent('CPU-FREQ', false);
433    }
434  }
435
436  connectedCallback(): void {
437    super.connectedCallback();
438    this.canvas = this.shadowRoot!.querySelector<HTMLCanvasElement>('#canvas');
439    this.scatterTipEL = this.shadowRoot!.querySelector<HTMLDivElement>('#tip');
440    this.ctx = this.canvas!.getContext('2d', { alpha: true });
441    this.labelsEL = this.shadowRoot!.querySelector<HTMLDivElement>('#shape');
442    resizeCanvas(this.canvas!);
443    this.originX = this.clientWidth * 0.1;
444    this.originY = this.clientHeight * 0.9;
445    this.finalX = this.clientWidth;
446    this.finalY = this.clientHeight * 0.1;
447    /*hover效果*/
448    this.canvas!.onmousemove = (event) => {
449      let pos: Object = {
450        x: event.offsetX,
451        y: event.offsetY,
452      };
453      let hoverPoint: Object | boolean = this.checkHover(this.options, pos);
454      /**
455       * 如果当前有聚焦点
456       */
457      if (hoverPoint) {
458        this.showTip(hoverPoint);
459        let samePoint: boolean = this.options!.hoverData === hoverPoint ? true : false;
460        if (!samePoint) {
461          this.resetHoverWithOffScreen();
462          this.options!.hoverData = hoverPoint;
463        }
464        this.paintHover();
465      } else {
466        //使用离屏canvas恢复
467        this.resetHoverWithOffScreen();
468        this.hideTip();
469      }
470    };
471  }
472
473  initElements(): void {
474    new ResizeObserver((entries, observer) => {
475      entries.forEach((it) => {
476        resizeCanvas(this.canvas!);
477        this.originX = this.clientWidth * 0.1;
478        this.originY = this.clientHeight * 0.95;
479        this.finalX = this.clientWidth * 0.9;
480        this.finalY = this.clientHeight * 0.1;
481        this.labelsEL!.innerText = '';
482        this.init();
483      });
484    }).observe(this);
485  }
486
487  initHtml(): string {
488    return (
489      `
490            <style>   
491            :host {
492                display: flex;
493                flex-direction: column;
494                overflow: hidden;
495                width: 100%;
496                height: 100%;
497            }
498            .shape.active {
499                display: block;
500                position: absolute;
501                left: 35%;    
502                z-index: 99;
503            }
504            #tip{
505                background-color: #f5f5f4;
506                border: 1px solid #fff;
507                border-radius: 5px;
508                color: #333322;
509                font-size: 8pt;
510                position: absolute;
511                display: none;
512                top: 0;
513                left: 0;
514                z-index: 99;
515                pointer-events: none;
516                user-select: none;
517                padding: 5px 10px;
518                box-shadow: 0 0 10px #22ffffff;
519            }
520            #root{
521                position:relative;
522            }
523            .bg_nodata{
524                background-repeat:no-repeat;
525                background-position:center;
526                background-image: url("img/pie_chart_no_data.png");
527            }
528            .bg_hasdata{
529                background-repeat:no-repeat;
530                background-position:center;
531            }
532            ` + this.dismantlingHtml()
533    );
534  }
535
536  /**
537   * 拆解initHtml大函数块
538   * @returns html
539   */
540  dismantlingHtml(): string {
541    return `
542      #labels{
543        display: grid;
544        grid-template-columns: auto auto auto auto auto;
545        width: 100%;
546        height: 25%;
547        box-sizing: border-box;
548        position: absolute;
549        bottom: 0px;
550        left: 0;
551        padding-left: 10px;
552        padding-right: 10px;
553        pointer-events: none;
554      }
555      .name{
556        flex: 1;
557        font-size: 9pt;
558        overflow: hidden;
559        white-space: nowrap;
560        text-overflow: ellipsis;
561        color: var(--dark-color1,#252525);
562        pointer-events: painted;
563      }
564      .label{
565        display: flex;
566        align-items: center;
567        max-lines: 1;
568        white-space: nowrap;
569        overflow: hidden;
570        padding-right: 5px;
571      }
572      .tag{
573        display: flex;
574        align-items: center;
575        justify-content: center;
576        width: 10px;
577        height: 10px;
578        border-radius: 5px;
579        margin-right: 5px;
580      }
581      </style>
582      <div id="root">
583          <div id="shape" class="shape active"></div>
584          <canvas id="canvas" style="top: 0;left: 0;z-index: 21;position: absolute"></canvas>
585          <div id="tip"></div>
586          <div id="labels"></div>
587      </div>`;
588  }
589}
590