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