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