• 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 { BaseElement, element } from '../../../../../base-ui/BaseElement';
17import { SelectionParam } from '../../../../bean/BoxSelection';
18import { debounce } from '../../../Utils';
19
20const paddingLeft = 100;
21const paddingBottom = 15;
22const xStart = 50; // x轴起始位置
23const barWidth = 2; // 柱子宽度
24const millisecond = 1_000_000;
25
26@element('tab-sample-instructions-totaltime-selection')
27export class TabPaneSampleInstructionTotalTime extends BaseElement {
28  private instructionChartEle: HTMLCanvasElement | undefined | null;
29  private ctx: CanvasRenderingContext2D | undefined | null;
30  private cacheData: Array<unknown> = [];
31  private canvasX = -1; // 鼠标当前所在画布x坐标
32  private canvasY = -1; // 鼠标当前所在画布y坐标
33  private startX = 0; // 画布相对于整个界面的x坐标
34  private startY = 0; // 画布相对于整个界面的y坐标
35  private hoverBar: unknown;
36  private onReadableData: Array<unknown> = [];
37  private hintContent = ''; //悬浮框内容
38  private floatHint: HTMLDivElement | undefined | null; //悬浮框
39  private canvasScrollTop = 0; // tab页上下滚动位置
40  private isUpdateCanvas = false;
41  private xCount = 0; //x轴刻度个数
42  private xMaxValue = 0; //x轴上数据最大值
43  private xSpacing = 50; //x轴间距
44  private xAvg = 0; //根据xMaxValue进行划分 用于x轴上刻度显示
45  private yAvg = 0; //根据yMaxValue进行划分 用于y轴上刻度显示
46
47  initHtml(): string {
48    return `
49      <style>
50        :host {
51          display: flex;
52        }
53        .frame-tip {
54          position: absolute;
55          left: 0;
56          background-color: white;
57          border: 1px solid #F9F9F9;
58          width: auto;
59          font-size: 14px;
60          color: #50809e;
61          padding: 2px 10px;
62          box-sizing: border-box;
63          display: none;
64          max-width: 200px;
65        }
66        .title {
67          font-size: 14px;
68          padding: 0 5px;
69        }
70        .bold {
71          font-weight: bold;
72        }
73      </style>
74      <canvas id="instruct-chart-canvas" height="280"></canvas>
75      <div id="float_hint" class="frame-tip"></div>
76    `;
77  }
78
79  initElements(): void {
80    this.instructionChartEle = this.shadowRoot?.querySelector('#instruct-chart-canvas');
81    this.ctx = this.instructionChartEle?.getContext('2d');
82    this.floatHint = this.shadowRoot?.querySelector('#float_hint');
83  }
84
85  set data(SampleParam: SelectionParam) {
86    // @ts-ignore
87    this.onReadableData = SampleParam.sampleData[0].property;
88    this.calInstructionRangeCount();
89  }
90
91  connectedCallback(): void {
92    super.connectedCallback();
93    this.parentElement!.onscroll = () => {
94      this.canvasScrollTop = this.parentElement!.scrollTop;
95      this.hideTip();
96    };
97    this.instructionChartEle!.onmousemove = (e): void => {
98      if (!this.isUpdateCanvas) {
99        this.updateCanvasCoord();
100      }
101      this.canvasX = e.clientX - this.startX;
102      this.canvasY = e.clientY - this.startY + this.canvasScrollTop;
103      this.onMouseMove();
104    };
105    this.instructionChartEle!.onmouseleave = () => {
106      this.hideTip();
107    };
108    this.listenerResize();
109  }
110
111  /**
112   * 更新canvas坐标
113   */
114  updateCanvasCoord(): void {
115    if (this.instructionChartEle instanceof HTMLCanvasElement) {
116      this.isUpdateCanvas = this.instructionChartEle.clientWidth !== 0;
117      if (this.instructionChartEle.getBoundingClientRect()) {
118        const box = this.instructionChartEle.getBoundingClientRect();
119        const D = this.parentElement!;
120        this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
121        this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop;
122      }
123    }
124  }
125
126  /**
127   * 获取鼠标悬停的函数
128   * @param nodes
129   * @param canvasX
130   * @param canvasY
131   * @returns
132   */
133  searchDataByCoord(nodes: unknown, canvasX: number, canvasY: number): unknown {
134    // @ts-ignore
135    for (let i = 0; i < nodes.length; i++) {
136      // @ts-ignore
137      const element = nodes[i];
138      if (this.isContains(element, canvasX, canvasY)) {
139        return element;
140      }
141    }
142    return null;
143  }
144
145  /**
146   * 鼠标移动
147   */
148  onMouseMove(): void {
149    const lastNode = this.hoverBar;
150    //查找鼠标所在的node
151    const searchResult = this.searchDataByCoord(this.cacheData, this.canvasX, this.canvasY);
152    if (searchResult) {
153      this.hoverBar = searchResult;
154      //鼠标悬浮的node未改变则不需重新渲染文字
155      if (searchResult !== lastNode) {
156        this.updateTipContent();
157      }
158      this.showTip();
159    } else {
160      this.hideTip();
161      this.hoverBar = undefined;
162    }
163  }
164
165  /**
166   *  隐藏悬浮框
167   */
168  hideTip(): void {
169    if (this.floatHint) {
170      this.floatHint.style.display = 'none';
171      this.instructionChartEle!.style.cursor = 'default';
172    }
173  }
174
175  /**
176   * 显示悬浮框
177   */
178  showTip(): void {
179    this.floatHint!.innerHTML = this.hintContent;
180    this.floatHint!.style.display = 'block';
181    this.instructionChartEle!.style.cursor = 'pointer';
182    let x = this.canvasX;
183    let y = this.canvasY - this.canvasScrollTop;
184    //右边的函数悬浮框显示在左侧
185    if (this.canvasX + this.floatHint!.clientWidth > (this.instructionChartEle!.clientWidth || 0)) {
186      x -= this.floatHint!.clientWidth - 1;
187    } else {
188      x += 30;
189    }
190    //最下边的函数悬浮框显示在上方
191    y -= this.floatHint!.clientHeight - 1;
192    this.floatHint!.style.transform = `translate(${x}px, ${y}px)`;
193  }
194
195  /**
196   * 更新悬浮框内容
197   */
198  updateTipContent(): void {
199    const hoverNode = this.hoverBar;
200    if (!hoverNode) {
201      return;
202    }
203    const detail = hoverNode!;
204    // @ts-ignore
205    this.hintContent = ` <span class="blod">${detail.instruct}</span></br><span>${parseFloat(
206      // @ts-ignore
207      detail.heightPer
208    )}</span> `;
209  }
210
211  /**
212   * 判断鼠标当前在那个函数上
213   * @param frame
214   * @param x
215   * @param y
216   * @returns
217   */
218  isContains(point: unknown, x: number, y: number): boolean {
219    // @ts-ignore
220    return x >= point.x && x <= point.x + 2 && point.y <= y && y <= point.y + point.height;
221  }
222
223  /**
224   * 统计onReadable数据各指令数个数
225   */
226  calInstructionRangeCount() {
227    if (this.onReadableData.length === 0) return;
228    this.cacheData.length = 0;
229    const count = this.onReadableData.length;
230    const onReadableData = this.onReadableData;
231    const instructionArray = onReadableData.reduce((pre: unknown, current: unknown) => {
232      // @ts-ignore
233      const dur = parseFloat(((current.end - current.begin) / millisecond).toFixed(1));
234      // @ts-ignore
235      (pre[dur] = pre[dur] || []).push(current);
236      return pre;
237    }, {});
238    this.ctx!.clearRect(0, 0, this.instructionChartEle!.width, this.instructionChartEle!.height);
239    this.instructionChartEle!.width = this.clientWidth;
240
241    this.xMaxValue =
242      // @ts-ignore
243      Object.keys(instructionArray)
244        .map((i) => Number(i))
245        .reduce((pre, cur) => Math.max(pre, cur), 0) + 5; // @ts-ignore
246    const yMaxValue = Object.values(instructionArray).reduce(
247      // @ts-ignore
248      (pre: number, cur: unknown) => Math.max(pre, Number((cur.length / count).toFixed(2))),
249      0
250    );
251    this.yAvg = Number(((yMaxValue / 5) * 1.5).toFixed(2));
252    const height = this.instructionChartEle!.height;
253    const width = this.instructionChartEle!.width;
254    this.drawLineLabelMarkers(width, height);
255    this.drawBar(instructionArray, height, count);
256  }
257
258  /**
259   * 绘制柱状图
260   * @param instructionData
261   * @param height
262   * @param count
263   */
264  drawBar(instructionData: unknown, height: number, count: number): void {
265    const yTotal = Number((this.yAvg * 5).toFixed(2));
266    const interval = Math.floor((height - paddingBottom) / 6); // @ts-ignore
267    for (const x in instructionData) {
268      const xNum = Number(x);
269      const xPosition = xStart + (xNum / (this.xCount * this.xAvg)) * (this.xCount * this.xSpacing) - barWidth / 2; // @ts-ignore
270      const yNum = Number((instructionData[x].length / count).toFixed(3));
271      const percent = Number((yNum / yTotal).toFixed(2));
272      const barHeight = (height - paddingBottom - interval) * percent;
273      this.drawRect(xPosition, height - paddingBottom - barHeight, barWidth, barHeight); // @ts-ignore
274      const existX = this.cacheData.find((i) => i.instruct === x);
275      if (!existX) {
276        this.cacheData.push({
277          instruct: x,
278          x: xPosition,
279          y: height - paddingBottom - barHeight,
280          height: barHeight,
281          heightPer: parseFloat((yNum * 100).toFixed(2)),
282        });
283      } else {
284        // @ts-ignore
285        existX.x = xPosition;
286      }
287    }
288  }
289
290  /**
291   * 绘制x y轴
292   * @param width
293   * @param height
294   */
295  drawLineLabelMarkers(width: number, height: number) {
296    this.ctx!.font = '12px Arial';
297    this.ctx!.lineWidth = 1;
298    this.ctx!.fillStyle = '#333';
299    this.ctx!.strokeStyle = '#ccc';
300
301    this.ctx!.fillText('时长 / ms', width - paddingLeft, height - paddingBottom);
302
303    //绘制x轴
304    this.drawLine(xStart, height - paddingBottom, width - paddingLeft, height - paddingBottom);
305    //绘制y轴
306    this.drawLine(xStart, 5, xStart, height - paddingBottom);
307    //绘制标记
308    this.drawMarkers(width, height);
309  }
310
311  /**
312   * 绘制横线
313   * @param x
314   * @param y
315   * @param X
316   * @param Y
317   */
318  drawLine(x: number, y: number, X: number, Y: number) {
319    this.ctx!.beginPath;
320    this.ctx!.moveTo(x, y);
321    this.ctx!.lineTo(X, Y);
322    this.ctx!.stroke();
323    this.ctx!.closePath();
324  }
325
326  /**
327   * 绘制x y轴刻度
328   * @param width
329   * @param height
330   */
331  drawMarkers(width: number, height: number) {
332    this.xCount = 0;
333    //绘制x轴锯齿
334    let serrateX = 50;
335    let yHeight = height - paddingBottom;
336    const clientWidth = width - paddingLeft - 50;
337    if (clientWidth > this.xMaxValue) {
338      this.xSpacing = Math.floor(clientWidth / 20);
339      this.xAvg = Math.ceil(this.xMaxValue / 20);
340    } else {
341      this.xSpacing = Math.floor(clientWidth / 10);
342      this.xAvg = Math.ceil(this.xMaxValue / 10);
343    }
344    while (serrateX <= clientWidth) {
345      this.xCount++;
346      serrateX += this.xSpacing;
347      this.drawLine(serrateX, yHeight, serrateX, yHeight + 5);
348    }
349    //绘制x轴刻度
350    this.ctx!.textAlign = 'center';
351    for (let i = 0; i <= this.xCount; i++) {
352      const x = xStart + i * this.xSpacing;
353      this.ctx!.fillText(`${i * this.xAvg}`, x, height);
354    }
355    //绘制y轴刻度
356    this.ctx!.textAlign = 'center';
357    const yPadding = Math.floor((height - paddingBottom) / 6);
358    for (let i = 0; i < 6; i++) {
359      const y = height - paddingBottom - i * yPadding;
360      if (i === 0) {
361        this.ctx!.fillText(`${i}%`, 30, y);
362      } else {
363        this.drawLine(xStart, y, width - paddingLeft, y);
364        this.ctx!.fillText(`${parseFloat((i * this.yAvg).toFixed(2)) * 100}%`, 30, y);
365      }
366    }
367  }
368
369  /**
370   * 监听页面size变化
371   */
372  listenerResize(): void {
373    new ResizeObserver(
374      debounce(() => {
375        if (this.instructionChartEle!.getBoundingClientRect()) {
376          const box = this.instructionChartEle!.getBoundingClientRect();
377          const element = this.parentElement!;
378          this.startX = box.left + Math.max(element.scrollLeft, document.body.scrollLeft) - element.clientLeft;
379          this.startY =
380            box.top + Math.max(element.scrollTop, document.body.scrollTop) - element.clientTop + this.canvasScrollTop;
381          this.calInstructionRangeCount();
382        }
383      }, 100)
384    ).observe(this.parentElement!);
385  }
386  /**
387   * 绘制方块
388   * @param x
389   * @param y
390   * @param X
391   * @param Y
392   */
393  drawRect(x: number, y: number, X: number, Y: number) {
394    this.ctx!.beginPath();
395    this.ctx!.rect(x, y, X, Y);
396    this.ctx!.fill();
397    this.ctx!.closePath();
398  }
399}
400