/* * Copyright (C) 2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { resizeCanvas } from '../helper.js'; import { BaseElement, element } from '../../BaseElement.js'; import { LitChartPieConfig } from './LitChartPieConfig.js'; import { isPointIsCircle, pieChartColors, randomRgbColor } from './LitChartPieData.js'; import { Utils } from '../../../trace/component/trace/base/Utils.js'; interface Rectangle { x: number; y: number; w: number; h: number; } class Sector { id?: any; obj?: any; key: any; value: any; startAngle?: number; endAngle?: number; startDegree?: number; endDegree?: number; color?: string; percent?: number; hover?: boolean; ease?: { initVal?: number; step?: number; process?: boolean; }; } @element('lit-chart-pie') export class LitChartPie extends BaseElement { private eleShape: Element | null | undefined; private pieTipEL: HTMLDivElement | null | undefined; private labelsEL: HTMLDivElement | null | undefined; canvas: HTMLCanvasElement | undefined | null; ctx: CanvasRenderingContext2D | undefined | null; litChartPieConfig: LitChartPieConfig | null | undefined; centerX: number | null | undefined; centerY: number | null | undefined; data: Sector[] = []; radius: number | undefined; private textRects: Rectangle[] = []; set config(litChartPieCfg: LitChartPieConfig | null | undefined) { if (!litChartPieCfg) return; this.litChartPieConfig = litChartPieCfg; (this.shadowRoot!.querySelector('#root') as HTMLDivElement).className = litChartPieCfg && litChartPieCfg.data.length > 0 ? 'bg_hasdata' : 'bg_nodata'; this.measure(); this.render(); } set dataSource(litChartPieArr: any[]) { if (this.litChartPieConfig) { this.litChartPieConfig.data = litChartPieArr; this.measure(); this.render(); } } showHover() { let hasHover = false; this.data.forEach((it) => { it.hover = it.obj.isHover; if (it.hover) { hasHover = true; } this.updateHoverItemStatus(it); if (it.hover) { this.showTip( this.centerX || 0, this.centerY || 0, this.litChartPieConfig!.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}` ); } }); if (!hasHover) { this.hideTip(); } this.render(); } measure() { if (!this.litChartPieConfig) return; this.data = []; this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10; let pieCfg = this.litChartPieConfig!; let startAngle = 0; let startDegree = 0; let full = Math.PI / 180; //每度 let fullDegree = 0; //每度 let sum = this.litChartPieConfig.data.reduce( (previousValue, currentValue) => currentValue[pieCfg.angleField] + previousValue, 0 ); this.labelsEL!.textContent = ''; let labelArray: string[] = []; this.litChartPieConfig.data.forEach((pieItem, index) => { let item: Sector = { id: `id-${Utils.uuid()}`, color: this.litChartPieConfig!.label.color ? this.litChartPieConfig!.label.color(pieItem) : pieChartColors[index % pieChartColors.length], obj: pieItem, key: pieItem[pieCfg.colorField], value: pieItem[pieCfg.angleField], startAngle: startAngle, endAngle: startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360), startDegree: startDegree, endDegree: startDegree + fullDegree + (pieItem[pieCfg.angleField] / sum) * 360, ease: { initVal: 0, step: (startAngle + full * ((pieItem[pieCfg.angleField] / sum) * 360)) / startDegree, process: true, }, }; this.data.push(item); startAngle += full * ((pieItem[pieCfg.angleField] / sum) * 360); startDegree += fullDegree + (pieItem[pieCfg.angleField] / sum) * 360; labelArray.push(``); }); this.labelsEL!.innerHTML = labelArray.join(''); } get config(): LitChartPieConfig | null | undefined { return this.litChartPieConfig; } connectedCallback() { super.connectedCallback(); this.eleShape = this.shadowRoot!.querySelector('#shape'); this.pieTipEL = this.shadowRoot!.querySelector('#tip'); this.labelsEL = this.shadowRoot!.querySelector('#labels'); this.canvas = this.shadowRoot!.querySelector('#canvas'); this.ctx = this.canvas!.getContext('2d', { alpha: true }); resizeCanvas(this.canvas!); this.radius = (Math.min(this.clientHeight, this.clientWidth) * 0.65) / 2 - 10; this.centerX = this.clientWidth / 2; this.centerY = this.clientHeight / 2 - 40; this.ctx?.translate(this.centerX, this.centerY); this.canvas!.onmouseout = (e) => { this.hideTip(); this.data.forEach((it) => { it.hover = false; this.updateHoverItemStatus(it); }); this.render(); }; //增加点击事件 this.canvas!.onclick = (ev) => { let rect = this.getBoundingClientRect(); let x = ev.pageX - rect.left - this.centerX!; let y = ev.pageY - rect.top - this.centerY!; if (isPointIsCircle(0, 0, x, y, this.radius!)) { let degree = this.computeDegree(x, y); this.data.forEach((it) => { if (degree >= it.startDegree! && degree <= it.endDegree!) { this.config?.angleClick?.(it.obj); } }); } }; this.canvas!.onmousemove = (ev) => { let rect = this.getBoundingClientRect(); let x = ev.pageX - rect.left - this.centerX!; let y = ev.pageY - rect.top - this.centerY!; if (isPointIsCircle(0, 0, x, y, this.radius!)) { let degree = this.computeDegree(x, y); this.data.forEach((it) => { it.hover = degree >= it.startDegree! && degree <= it.endDegree!; this.updateHoverItemStatus(it); it.obj.isHover = it.hover; if (it.hover && this.litChartPieConfig) { this.litChartPieConfig.hoverHandler?.(it.obj); this.showTip( ev.pageX - rect.left + 10, ev.pageY - this.offsetTop - 10, this.litChartPieConfig.tip ? this.litChartPieConfig!.tip(it) : `${it.key}: ${it.value}` ); } }); } else { this.hideTip(); this.data.forEach((it) => { it.hover = false; it.obj.isHover = false; this.updateHoverItemStatus(it); }); this.litChartPieConfig?.hoverHandler?.(undefined); } this.render(); }; this.render(); } updateHoverItemStatus(item: any) { let label = this.shadowRoot!.querySelector(`#${item.id}`); if (label) { (label as HTMLLabelElement).style.boxShadow = item.hover ? '0 0 5px #22ffffff' : ''; } } computeDegree(x: number, y: number) { let degree = (360 * Math.atan(y / x)) / (2 * Math.PI); if (x >= 0 && y >= 0) { degree = degree; } else if (x < 0 && y >= 0) { degree = 180 + degree; } else if (x < 0 && y < 0) { degree = 180 + degree; } else { degree = 270 + (90 + degree); } return degree; } initElements(): void { new ResizeObserver((entries, observer) => { entries.forEach((it) => { resizeCanvas(this.canvas!); this.centerX = this.clientWidth / 2; this.centerY = this.clientHeight / 2 - 40; this.ctx?.translate(this.centerX, this.centerY); this.measure(); this.render(); }); }).observe(this); } render(ease: boolean = true) { if (!this.canvas || !this.litChartPieConfig) return; if (this.radius! <= 0) return; this.ctx?.clearRect(0 - this.centerX!, 0 - this.centerY!, this.clientWidth, this.clientHeight); this.data.forEach((it) => { this.ctx!.beginPath(); this.ctx!.fillStyle = it.color as string; this.ctx!.strokeStyle = this.data.length > 1 ? '#fff' : (it.color as string); this.ctx?.moveTo(0, 0); if (it.hover) { this.ctx!.lineWidth = 1; this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); } else { this.ctx!.lineWidth = 1; if (ease) { if (it.ease!.initVal! < it.endAngle! - it.startAngle!) { it.ease!.process = true; this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.startAngle! + it.ease!.initVal!, false); it.ease!.initVal! += it.ease!.step!; } else { it.ease!.process = false; this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); } } else { this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); } } this.ctx?.lineTo(0, 0); this.ctx?.fill(); this.ctx!.stroke(); this.ctx?.closePath(); }); this.data .filter((it) => it.hover) .forEach((it) => { this.ctx!.beginPath(); this.ctx!.fillStyle = it.color as string; this.ctx!.lineWidth = 1; this.ctx?.moveTo(0, 0); this.ctx!.arc(0, 0, this.radius!, it.startAngle!, it.endAngle!, false); this.ctx?.lineTo(0, 0); this.ctx!.strokeStyle = this.data.length > 1 ? '#000' : (it.color as string); this.ctx!.stroke(); this.ctx?.closePath(); }); this.textRects = []; if (this.litChartPieConfig.showChartLine) { this.data.forEach((dataItem) => { let text = `${dataItem.value}`; let metrics = this.ctx!.measureText(text); let textWidth = metrics.width; let textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; this.ctx!.beginPath(); this.ctx!.strokeStyle = dataItem.color!; this.ctx!.fillStyle = '#595959'; let deg = dataItem.startDegree! + (dataItem.endDegree! - dataItem.startDegree!) / 2; let dep = 25; let x1 = 0 + this.radius! * Math.cos((deg * Math.PI) / 180); let y1 = 0 + this.radius! * Math.sin((deg * Math.PI) / 180); let x2 = 0 + (this.radius! + 13) * Math.cos((deg * Math.PI) / 180); let y2 = 0 + (this.radius! + 13) * Math.sin((deg * Math.PI) / 180); let x3 = 0 + (this.radius! + dep) * Math.cos((deg * Math.PI) / 180); let y3 = 0 + (this.radius! + dep) * Math.sin((deg * Math.PI) / 180); this.ctx!.moveTo(x1, y1); this.ctx!.lineTo(x2, y2); this.ctx!.stroke(); let rect = this.correctRect({ x: x3 - textWidth / 2, y: y3 + textHeight / 2, w: textWidth, h: textHeight, }); this.ctx?.fillText(text, rect.x, rect.y); this.ctx?.closePath(); }); } if (this.data.filter((it) => it.ease!.process).length > 0) { requestAnimationFrame(() => this.render(ease)); } } correctRect(pieRect: Rectangle): Rectangle { if (this.textRects.length == 0) { this.textRects.push(pieRect); return pieRect; } else { let rectangles = this.textRects.filter((it) => this.intersect(it, pieRect).cross); if (rectangles.length == 0) { this.textRects.push(pieRect); return pieRect; } else { let it = rectangles[0]; let inter = this.intersect(it, pieRect); if (inter.direction == 'Right') { pieRect.x += inter.crossW; } else if (inter.direction == 'Bottom') { pieRect.y += inter.crossH; } else if (inter.direction == 'Left') { pieRect.x -= inter.crossW; } else if (inter.direction == 'Top') { pieRect.y -= inter.crossH; } else if (inter.direction == 'Right-Top') { pieRect.y -= inter.crossH; } else if (inter.direction == 'Right-Bottom') { pieRect.y += inter.crossH; } else if (inter.direction == 'Left-Top') { pieRect.y -= inter.crossH; } else if (inter.direction == 'Left-Bottom') { pieRect.y += inter.crossH; } this.textRects.push(pieRect); return pieRect; } } } intersect( r1: Rectangle, rect: Rectangle ): { cross: boolean; direction: string; crossW: number; crossH: number; } { let cross: boolean; let direction: string; let crossW: number; let crossH: number; let maxX = r1.x + r1.w > rect.x + rect.w ? r1.x + r1.w : rect.x + rect.w; let maxY = r1.y + r1.h > rect.y + rect.h ? r1.y + r1.h : rect.y + rect.h; let minX = r1.x < rect.x ? r1.x : rect.x; let minY = r1.y < rect.y ? r1.y : rect.y; if (maxX - minX < rect.w + r1.w && maxY - minY < r1.h + rect.h) { cross = true; } else { cross = false; } crossW = Math.abs(maxX - minX - (rect.w + r1.w)); crossH = Math.abs(maxY - minY - (rect.y + r1.y)); if (rect.x > r1.x) { //right if (rect.y > r1.y) { //bottom direction = 'Right-Bottom'; } else if (rect.y == r1.y) { //middle direction = 'Right'; } else { //top direction = 'Right-Top'; } } else if (rect.x < r1.x) { //left if (rect.y > r1.y) { //bottom direction = 'Left-Bottom'; } else if (rect.y == r1.y) { //middle direction = 'Left'; } else { //top direction = 'Left-Top'; } } else { if (rect.y > r1.y) { //bottom direction = 'Bottom'; } else if (rect.y == r1.y) { //middle direction = 'Right'; //superposition default right } else { //top direction = 'Top'; } } return { cross, direction, crossW, crossH, }; } showTip(x: number, y: number, msg: string) { this.pieTipEL!.style.display = 'flex'; this.pieTipEL!.style.top = `${y}px`; this.pieTipEL!.style.left = `${x}px`; this.pieTipEL!.innerHTML = msg; } hideTip() { this.pieTipEL!.style.display = 'none'; } initHtml(): string { return `
`; } }