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