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