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 { resizeCanvas } from '../helper'; 17import { BaseElement, element } from '../../BaseElement'; 18import { LitChartPieConfig } from './LitChartPieConfig'; 19import { isPointIsCircle, pieChartColors, randomRgbColor } from './LitChartPieData'; 20import { Utils } from '../../../trace/component/trace/base/Utils'; 21 22interface Rectangle { 23 x: number; 24 y: number; 25 w: number; 26 h: number; 27} 28 29class Sector { 30 id?: any; 31 obj?: any; 32 key: any; 33 value: any; 34 startAngle?: number; 35 endAngle?: number; 36 startDegree?: number; 37 endDegree?: number; 38 color?: string; 39 percent?: number; 40 hover?: boolean; 41 ease?: { 42 initVal?: number; 43 step?: number; 44 process?: boolean; 45 }; 46} 47 48const initHtmlStyle = ` 49 <style> 50 :host { 51 display: flex; 52 flex-direction: column; 53 overflow: hidden; 54 width: 100%; 55 height: 100%; 56 } 57 .shape.active { 58 animation: color 3.75 both; 59 } 60 @keyframes color { 61 0% { background-color: white; } 62 100% { background-color: black; } 63 } 64 #tip{ 65 background-color: #f5f5f4; 66 border: 1px solid #fff; 67 border-radius: 5px; 68 color: #333322; 69 font-size: 8pt; 70 position: absolute; 71 display: none; 72 top: 0; 73 left: 0; 74 z-index: 99; 75 pointer-events: none; 76 user-select: none; 77 padding: 5px 10px; 78 box-shadow: 0 0 10px #22ffffff; 79 } 80 #root{ 81 position:relative; 82 } 83 .bg_nodata{ 84 background-repeat:no-repeat; 85 background-position:center; 86 background-image: url("img/pie_chart_no_data.png"); 87 } 88 .bg_hasdata{ 89 background-repeat:no-repeat; 90 background-position:center; 91 } 92 93 #labels{ 94 display: grid; 95 grid-template-columns: auto auto auto auto auto; 96 /*justify-content: center;*/ 97 /*align-items: center;*/ 98 width: 100%; 99 height: 25%; 100 box-sizing: border-box; 101 position: absolute; 102 bottom: 0px; 103 left: 0; 104 /*margin: 0px 10px;*/ 105 padding-left: 10px; 106 padding-right: 10px; 107 pointer-events: none ; 108 } 109 .name{ 110 flex: 1; 111 font-size: 9pt; 112 overflow: hidden; 113 white-space: nowrap; 114 text-overflow: ellipsis; 115 /*color: #666;*/ 116 color: var(--dark-color1,#252525); 117 pointer-events: painted; 118 } 119 .label{ 120 display: flex; 121 align-items: center; 122 max-lines: 1; 123 white-space: nowrap; 124 overflow: hidden; 125 padding-right: 5px; 126 } 127 .tag{ 128 display: flex; 129 align-items: center; 130 justify-content: center; 131 width: 10px; 132 height: 10px; 133 border-radius: 5px; 134 margin-right: 5px; 135 } 136 </style> 137 ` 138 139@element('lit-chart-pie') 140export class LitChartPie extends BaseElement { 141 private eleShape: Element | null | undefined; 142 private pieTipEL: HTMLDivElement | null | undefined; 143 private labelsEL: HTMLDivElement | null | undefined; 144 canvas: HTMLCanvasElement | undefined | null; 145 ctx: CanvasRenderingContext2D | undefined | null; 146 litChartPieConfig: LitChartPieConfig | null | undefined; 147 centerX: number | null | undefined; 148 centerY: number | null | undefined; 149 data: Sector[] = []; 150 radius: number | undefined; 151 private textRects: Rectangle[] = []; 152 153 set config(litChartPieCfg: LitChartPieConfig | null | undefined) { 154 if (!litChartPieCfg) return; 155 this.litChartPieConfig = litChartPieCfg; 156 (this.shadowRoot!.querySelector('#root') as HTMLDivElement).className = 157 litChartPieCfg && litChartPieCfg.data.length > 0 ? 'bg_hasdata' : 'bg_nodata'; 158 this.measure(); 159 this.render(); 160 } 161 162 set dataSource(litChartPieArr: any[]) { 163 if (this.litChartPieConfig) { 164 this.litChartPieConfig.data = litChartPieArr; 165 this.measure(); 166 this.render(); 167 } 168 } 169 170 showHover() { 171 let hasHover = false; 172 this.data.forEach((it) => { 173 it.hover = it.obj.isHover; 174 if (it.hover) { 175 hasHover = true; 176 } 177 this.updateHoverItemStatus(it); 178 if (it.hover) { 179 this.showTip( 180 this.centerX || 0, 181 this.centerY || 0, 182 this.litChartPieConfig!.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}` 183 ); 184 } 185 }); 186 if (!hasHover) { 187 this.hideTip(); 188 } 189 this.render(); 190 } 191 192 measureInitialize():void{ 193 this.data = []; 194 this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10; 195 this.labelsEL!.textContent = ''; 196 } 197 198 measure() { 199 if (!this.litChartPieConfig) return; 200 this.measureInitialize(); 201 let pieCfg = this.litChartPieConfig!; 202 let startAngle = 0; 203 let startDegree = 0; 204 let full = Math.PI / 180; //每度 205 let fullDegree = 0; //每度 206 let sum = this.litChartPieConfig.data.reduce( 207 (previousValue, currentValue) => currentValue[pieCfg.angleField] + previousValue, 208 0 209 ); 210 let labelArray: string[] = []; 211 this.litChartPieConfig.data.forEach((pieItem, index) => { 212 let item: Sector = { 213 id: `id-${Utils.uuid()}`, 214 color: this.litChartPieConfig!.label.color 215 ? this.litChartPieConfig!.label.color(pieItem) 216 : pieChartColors[index % pieChartColors.length], 217 obj: pieItem, 218 key: pieItem[pieCfg.colorField], 219 value: pieItem[pieCfg.angleField], 220 startAngle: startAngle, 221 endAngle: startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360), 222 startDegree: startDegree, 223 endDegree: startDegree + fullDegree + (pieItem[pieCfg.angleField] / sum) * 360, 224 ease: { 225 initVal: 0, 226 step: (startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360)) / startDegree, 227 process: true, 228 }, 229 }; 230 this.data.push(item); 231 startAngle += full * ((pieItem[pieCfg.angleField] / sum) * 360); 232 startDegree += fullDegree + (pieItem[pieCfg.angleField] / sum) * 360; 233 let colorFieldValue = item.obj[pieCfg.colorField]; 234 if (this.config?.colorFieldTransferHandler) { 235 colorFieldValue = this.config.colorFieldTransferHandler(colorFieldValue); 236 } 237 labelArray.push(`<label class="label"> 238 <div style="display: flex;flex-direction: row;margin-left: 5px;align-items: center;overflow: hidden;text-overflow: ellipsis" 239 id="${item.id}"> 240 <div class="tag" style="background-color: ${item.color}"></div> 241 <span class="name">${colorFieldValue}</span> 242 </div> 243 </label>`); 244 }); 245 this.labelsEL!.innerHTML = labelArray.join(''); 246 } 247 248 get config(): LitChartPieConfig | null | undefined { 249 return this.litChartPieConfig; 250 } 251 252 addCanvasOnmousemoveEvent():void{ 253 this.canvas!.onmousemove = (ev) => { 254 let rect = this.getBoundingClientRect(); 255 let x = ev.pageX - rect.left - this.centerX!; 256 let y = ev.pageY - rect.top - this.centerY!; 257 if (isPointIsCircle(0, 0, x, y, this.radius!)) { 258 let degree = this.computeDegree(x, y); 259 this.data.forEach((it) => { 260 it.hover = degree >= it.startDegree! && degree <= it.endDegree!; 261 this.updateHoverItemStatus(it); 262 it.obj.isHover = it.hover; 263 if (it.hover && this.litChartPieConfig) { 264 this.litChartPieConfig.hoverHandler?.(it.obj); 265 this.showTip( 266 ev.pageX - rect.left + 10, 267 ev.pageY - this.offsetTop - 10, 268 this.litChartPieConfig.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}` 269 ); 270 } 271 }); 272 } else { 273 this.hideTip(); 274 this.data.forEach((it) => { 275 it.hover = false; 276 it.obj.isHover = false; 277 this.updateHoverItemStatus(it); 278 }); 279 this.litChartPieConfig?.hoverHandler?.(undefined); 280 } 281 this.render(); 282 }; 283 } 284 connectedCallback() { 285 super.connectedCallback(); 286 this.eleShape = this.shadowRoot!.querySelector<Element>('#shape'); 287 this.pieTipEL = this.shadowRoot!.querySelector<HTMLDivElement>('#tip'); 288 this.labelsEL = this.shadowRoot!.querySelector<HTMLDivElement>('#labels'); 289 this.canvas = this.shadowRoot!.querySelector<HTMLCanvasElement>('#canvas'); 290 this.ctx = this.canvas!.getContext('2d', { alpha: true }); 291 resizeCanvas(this.canvas!); 292 this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10; 293 this.centerX = this.clientWidth / 2; 294 this.centerY = this.clientHeight / 2 - 40; 295 this.ctx?.translate(this.centerX, this.centerY); 296 this.canvas!.onmouseout = (e) => { 297 this.hideTip(); 298 this.data.forEach((it) => { 299 it.hover = false; 300 this.updateHoverItemStatus(it); 301 }); 302 this.render(); 303 }; 304 //增加点击事件 305 this.canvas!.onclick = (ev) => { 306 let rect = this.getBoundingClientRect(); 307 let x = ev.pageX - rect.left - this.centerX!; 308 let y = ev.pageY - rect.top - this.centerY!; 309 if (isPointIsCircle(0, 0, x, y, this.radius!)) { 310 let degree = this.computeDegree(x, y); 311 this.data.forEach((it) => { 312 if (degree >= it.startDegree! && degree <= it.endDegree!) { 313 this.config?.angleClick?.(it.obj); 314 } 315 }); 316 } 317 }; 318 this.addCanvasOnmousemoveEvent(); 319 this.render(); 320 } 321 322 updateHoverItemStatus(item: any) { 323 let label = this.shadowRoot!.querySelector(`#${item.id}`); 324 if (label) { 325 (label as HTMLLabelElement).style.boxShadow = item.hover ? '0 0 5px #22ffffff' : ''; 326 } 327 } 328 329 computeDegree(x: number, y: number) { 330 let degree = (360 * Math.atan(y / x)) / (2 * Math.PI); 331 if (x >= 0 && y >= 0) { 332 degree = degree; 333 } else if (x < 0 && y >= 0) { 334 degree = 180 + degree; 335 } else if (x < 0 && y < 0) { 336 degree = 180 + degree; 337 } else { 338 degree = 270 + (90 + degree); 339 } 340 return degree; 341 } 342 343 initElements(): void { 344 new ResizeObserver((entries, observer) => { 345 entries.forEach((it) => { 346 resizeCanvas(this.canvas!); 347 this.centerX = this.clientWidth / 2; 348 this.centerY = this.clientHeight / 2 - 40; 349 this.ctx?.translate(this.centerX, this.centerY); 350 this.measure(); 351 this.render(); 352 }); 353 }).observe(this); 354 } 355 356 handleData():void{ 357 this.textRects = []; 358 if (this.litChartPieConfig!.showChartLine) { 359 this.data.forEach((dataItem) => { 360 let text = `${dataItem.value}`; 361 let metrics = this.ctx!.measureText(text); 362 let textWidth = metrics.width; 363 let textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; 364 this.ctx!.beginPath(); 365 this.ctx!.strokeStyle = dataItem.color!; 366 this.ctx!.fillStyle = '#595959'; 367 let deg = dataItem.startDegree! + (dataItem.endDegree! - dataItem.startDegree!) / 2; 368 let dep = 25; 369 let x1 = 0 + this.radius! * Math.cos((deg * Math.PI) / 180); 370 let y1 = 0 + this.radius! * Math.sin((deg * Math.PI) / 180); 371 let x2 = 0 + (this.radius! + 13) * Math.cos((deg * Math.PI) / 180); 372 let y2 = 0 + (this.radius! + 13) * Math.sin((deg * Math.PI) / 180); 373 let x3 = 0 + (this.radius! + dep) * Math.cos((deg * Math.PI) / 180); 374 let y3 = 0 + (this.radius! + dep) * Math.sin((deg * Math.PI) / 180); 375 this.ctx!.moveTo(x1, y1); 376 this.ctx!.lineTo(x2, y2); 377 this.ctx!.stroke(); 378 let rect = this.correctRect({ 379 x: x3 - textWidth / 2, 380 y: y3 + textHeight / 2, 381 w: textWidth, 382 h: textHeight, 383 }); 384 this.ctx?.fillText(text, rect.x, rect.y); 385 this.ctx?.closePath(); 386 }); 387 } 388 } 389 390 render(ease: boolean = true) { 391 if (!this.canvas || !this.litChartPieConfig) return; 392 if (this.radius! <= 0) return; 393 this.ctx?.clearRect(0 - this.centerX!, 0 - this.centerY!, this.clientWidth, this.clientHeight); 394 this.data.forEach((it) => { 395 this.ctx!.beginPath(); 396 this.ctx!.fillStyle = it.color as string; 397 this.ctx!.strokeStyle = this.data.length > 1 ? '#fff' : (it.color as string); 398 this.ctx?.moveTo(0, 0); 399 if (it.hover) { 400 this.ctx!.lineWidth = 1; 401 this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); 402 } else { 403 this.ctx!.lineWidth = 1; 404 if (ease) { 405 if (it.ease!.initVal! < it.endAngle! - it.startAngle!) { 406 it.ease!.process = true; 407 this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.startAngle! + it.ease!.initVal!, false); 408 it.ease!.initVal! += it.ease!.step!; 409 } else { 410 it.ease!.process = false; 411 this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); 412 } 413 } else { 414 this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); 415 } 416 } 417 this.ctx?.lineTo(0, 0); 418 this.ctx?.fill(); 419 this.ctx!.stroke(); 420 this.ctx?.closePath(); 421 }); 422 this.setData(ease); 423 } 424 425 setData(ease: boolean):void{ 426 this.data 427 .filter((it) => it.hover) 428 .forEach((it) => { 429 this.ctx!.beginPath(); 430 this.ctx!.fillStyle = it.color as string; 431 this.ctx!.lineWidth = 1; 432 this.ctx?.moveTo(0, 0); 433 this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); 434 this.ctx?.lineTo(0, 0); 435 this.ctx!.strokeStyle = this.data.length > 1 ? '#000' : (it.color as string); 436 this.ctx!.stroke(); 437 this.ctx?.closePath(); 438 }); 439 this.handleData(); 440 if (this.data.filter((it) => it.ease!.process).length > 0) { 441 requestAnimationFrame(() => this.render(ease)); 442 } 443 } 444 445 correctRect(pieRect: Rectangle): Rectangle { 446 if (this.textRects.length == 0) { 447 this.textRects.push(pieRect); 448 return pieRect; 449 } else { 450 let rectangles = this.textRects.filter((it) => this.intersect(it, pieRect).cross); 451 if (rectangles.length == 0) { 452 this.textRects.push(pieRect); 453 return pieRect; 454 } else { 455 let it = rectangles[0]; 456 let inter = this.intersect(it, pieRect); 457 if (inter.direction == 'Right') { 458 pieRect.x += inter.crossW; 459 } else if (inter.direction == 'Bottom') { 460 pieRect.y += inter.crossH; 461 } else if (inter.direction == 'Left') { 462 pieRect.x -= inter.crossW; 463 } else if (inter.direction == 'Top') { 464 pieRect.y -= inter.crossH; 465 } else if (inter.direction == 'Right-Top') { 466 pieRect.y -= inter.crossH; 467 } else if (inter.direction == 'Right-Bottom') { 468 pieRect.y += inter.crossH; 469 } else if (inter.direction == 'Left-Top') { 470 pieRect.y -= inter.crossH; 471 } else if (inter.direction == 'Left-Bottom') { 472 pieRect.y += inter.crossH; 473 } 474 this.textRects.push(pieRect); 475 return pieRect; 476 } 477 } 478 } 479 480 intersect( 481 r1: Rectangle, 482 rect: Rectangle 483 ): { 484 cross: boolean; 485 direction: string; 486 crossW: number; 487 crossH: number; 488 } { 489 let cross: boolean; 490 let direction: string = ''; 491 let crossW: number; 492 let crossH: number; 493 let maxX = r1.x + r1.w > rect.x + rect.w ? r1.x + r1.w : rect.x + rect.w; 494 let maxY = r1.y + r1.h > rect.y + rect.h ? r1.y + r1.h : rect.y + rect.h; 495 let minX = r1.x < rect.x ? r1.x : rect.x; 496 let minY = r1.y < rect.y ? r1.y : rect.y; 497 cross = maxX - minX < rect.w + r1.w && maxY - minY < r1.h + rect.h; 498 crossW = Math.abs(maxX - minX - (rect.w + r1.w)); 499 crossH = Math.abs(maxY - minY - (rect.y + r1.y)); 500 if (rect.x > r1.x) { 501 if (rect.y > r1.y) { 502 direction = 'Right-Bottom'; 503 } else if (rect.y == r1.y) { 504 direction = 'Right'; 505 } else { 506 direction = 'Right-Top'; 507 } 508 } else if (rect.x < r1.x) { 509 if (rect.y > r1.y) { 510 direction = 'Left-Bottom'; 511 } else if (rect.y == r1.y) { 512 direction = 'Left'; 513 } else { 514 direction = 'Left-Top'; 515 } 516 } else { 517 direction = this.rectSuperposition(rect,r1); 518 } 519 return { 520 cross, 521 direction, 522 crossW, 523 crossH, 524 }; 525 } 526 527 rectSuperposition(rect: Rectangle,r1: Rectangle):string{ 528 if (rect.y > r1.y) { 529 return 'Bottom'; 530 } else if (rect.y == r1.y) { 531 return 'Right'; //superposition default right 532 } else { 533 return 'Top'; 534 } 535 } 536 537 showTip(x: number, y: number, msg: string) { 538 this.pieTipEL!.style.display = 'flex'; 539 this.pieTipEL!.style.top = `${y}px`; 540 this.pieTipEL!.style.left = `${x}px`; 541 this.pieTipEL!.innerHTML = msg; 542 } 543 544 hideTip() { 545 this.pieTipEL!.style.display = 'none'; 546 } 547 548 initHtml(): string { 549 return ` 550 ${initHtmlStyle} 551 <div id="root"> 552 <div id="shape" class="shape active"></div> 553 <canvas id="canvas" style="top: 0;left: 0;z-index: 21"></canvas> 554 <div id="tip"></div> 555 <div id="labels"></div> 556 </div>`; 557 } 558} 559