/* * Copyright (C) 2023 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'; import { BaseElement, element } from '../../BaseElement'; import { LitChartScatterConfig } from './LitChartScatterConfig'; @element('lit-chart-scatter') export class LitChartScatter extends BaseElement { private scatterTipEL: HTMLDivElement | null | undefined; private labelsEL: HTMLDivElement | null | undefined; canvas: HTMLCanvasElement | undefined | null; canvas2: HTMLCanvasElement | undefined | null; ctx: CanvasRenderingContext2D | undefined | null; originX: number = 0; finalX: number = 0; originY: number = 0; finalY: number = 0; options: LitChartScatterConfig | undefined; set config(LitChartScatterConfig: LitChartScatterConfig) { this.options = LitChartScatterConfig; this.init(); } init(): void { if (this.options) { // 清楚上一次绘制的数据 this.ctx?.clearRect(0, 0, this.clientWidth, this.clientHeight); this.drawBackground(); this.drawScatterChart(this.options); //使用off-screen-canvas保存绘制的像素点 this.setOffScreen(); this.labelsEL!.innerText = this.options.title; } } // 使用离屏技术保存绘制的像素点 setOffScreen(): void { this.canvas2 = document.createElement('canvas'); this.canvas2.height = this.clientHeight; this.canvas2.width = this.clientWidth; let context2: CanvasRenderingContext2D | null = this.canvas2.getContext('2d'); if (this.canvas?.width !== 0 && this.canvas?.height !== 0) { context2!.drawImage(this.canvas!, 0, 0); } } /*绘制渐变色背景*/ drawBackground(): void { let w: number = this.clientWidth; let h: number = this.clientHeight; let color: CanvasGradient = this.ctx?.createRadialGradient(w / 2, h / 2, 0.2 * w, w / 2, h / 2, 0.5 * w)!; color?.addColorStop(0, '#eaeaea'); color?.addColorStop(1, '#ccc'); if (this.options) { this.options!.globalGradient = color; } this.ctx?.save(); this.ctx!.fillStyle = color; this.ctx?.fillRect(0, 0, w, h); this.ctx?.restore(); } /** * 绘制散点图 */ drawScatterChart(options: LitChartScatterConfig): void { this.drawAxis(options); //绘制坐标轴 this.drawYLabels(options); //绘制y轴坐标 this.drawXLabels(options); //绘制x轴坐标 let drawload: boolean = false; if (options) { drawload = options.drawload; } if (drawload) { let load: Array = []; if (options) { load = options.load; this.drawBalanceLine(load); //绘制均衡线 this.drawLoadLine(load); //绘制最大负载线 } } this.drawData(options); //绘制散点图 } /** * 绘制坐标轴 */ drawAxis(options: LitChartScatterConfig): void { let text: Array = new Array(); if (options) { text = options.axisLabel; } this.ctx!.font = '10px KATTI'; this.ctx!.fillStyle = '#000000'; this.ctx!.strokeStyle = '#000000'; // 画x轴 this.ctx?.beginPath(); this.ctx?.moveTo(this.originX, this.originY); this.ctx?.lineTo(this.finalX, this.originY); this.ctx?.fillText(text[0], this.finalX, this.originY); this.ctx?.stroke(); // 画Y轴 this.ctx?.beginPath(); this.ctx?.moveTo(this.originX, this.originY); this.ctx?.lineTo(this.originX, this.finalY); this.ctx?.fillText(text[1], this.originX - 20, this.finalY - 10); this.ctx?.stroke(); } /** * 绘制y轴坐标 */ drawYLabels(options: LitChartScatterConfig): void { const AXAIS_DELTA: number = 5; const QUYU: number = 100; // 添加原点刻度 this.ctx!.font = '12px KATTI'; this.ctx!.fillStyle = '#000000'; this.ctx!.strokeStyle = '#000000'; this.ctx?.fillText('0', this.originX - AXAIS_DELTA, this.originY + AXAIS_DELTA * 2); let yAxis: Array = []; if (options) { yAxis = options.yAxisLabel; } // 画Y轴坐标尺 for (let i = 0; i < yAxis.length; i++) { let length1: number = (this.originY - this.finalY - ((this.originY - this.finalY) % QUYU)) * (yAxis[i] / yAxis[yAxis.length - 1]); let length2: number = this.originY - length1; let text: string = yAxis[i].toString(); let x: number = this.originX - this.ctx?.measureText(text).width! - AXAIS_DELTA; this.ctx?.beginPath(); this.ctx?.moveTo(this.originX, length2); this.ctx?.lineTo(this.originX + AXAIS_DELTA, length2); this.ctx?.fillText(text, x, length2 + AXAIS_DELTA); this.ctx?.stroke(); } } /** * 绘制x轴坐标 */ drawXLabels(options: LitChartScatterConfig): void { // 画X轴坐标尺 this.ctx!.fillStyle = '#000000'; this.ctx!.strokeStyle = '#000000'; const QUYU: number = 100; const DELTA: number = 5; let xAxis: Array = []; if (options) { xAxis = options.xAxisLabel; } for (let i = 0; i < xAxis.length; i++) { let length3: number = (this.finalX - this.originX - ((this.finalX - this.originX) % QUYU)) * (xAxis[i] / xAxis[xAxis.length - 1]); let length4: number = this.originX + length3; this.ctx?.beginPath(); this.ctx?.moveTo(length4, this.originY); this.ctx?.lineTo(length4, this.originY - DELTA); this.ctx?.fillText(xAxis[i].toString(), length4 - DELTA * 3, this.originY + DELTA * 2); this.ctx?.stroke(); } } /** * 绘制数据 */ drawData(options: LitChartScatterConfig): void { let data: Array>> = []; let yAxis: Array = []; let xAxis: Array = []; let colorPool: Array = new Array(); let colorPoolText: Array = new Array(); let rectY: number = this.clientHeight * 0.05; const QUYU: number = 100; const WIDTH_DELTA: number = 70; if (options) { data = options.data; yAxis = options.yAxisLabel; xAxis = options.xAxisLabel; colorPool = options.colorPool(); colorPoolText = options.colorPoolText(); options.paintingData = []; } let xLength: number = this.finalX - this.originX - ((this.finalX - this.originX) % QUYU); let yLength: number = this.originY - this.finalY - ((this.originY - this.finalY) % QUYU); for (let i = 0; i < data.length; i++) { for (let j = 0; j < data[i].length; j++) { // 打点x坐标 let x: number = this.originX + (data[i][j][0] / xAxis[xAxis.length - 1]) * xLength; // 打点y坐标 let y: number = this.originY - (data[i][j][1] / yAxis[yAxis.length - 1]) * yLength; let r: number = 6; if (i > 0) { options.paintingData[data[i][j][2] - 1] = { x, y, r, c: data[i][j], color: colorPool[i], }; } else { options.paintingData.push({ x, y, r, c: data[i][j], color: colorPool[i], }); } this.drawCycle(x, y, r, 0.8, colorPool[i]); } if (data[i].length) { rectY = rectY + 20; this.ctx?.fillText(colorPoolText[i] + ': ', this.clientWidth - WIDTH_DELTA, rectY + 4); this.drawCycle(this.clientWidth - QUYU / 5, rectY, 7.5, 0.8, colorPool[i]); } } } /** * 画圆点 */ drawCycle(x: number, y: number, r: number, transparency: number, color: string): void { this.ctx!.fillStyle = color; this.ctx?.beginPath(); this.ctx!.globalAlpha = transparency; this.ctx?.arc(x, y, r, 0, Math.PI * 2, true); this.ctx?.closePath(); this.ctx?.fill(); } /** * 绘制最大负载线 */ drawLoadLine(data: Array): void { let maxXAxis: number = 1; const QUYU: number = 100; const FOR_VALUE = 60; if (this.options) { maxXAxis = this.options.xAxisLabel[this.options.xAxisLabel.length - 1]; } // data[1]用来标注n Hz负载线 let addr1: number = this.originX + (this.finalX - this.originX - ((this.finalX - this.originX) % QUYU)) * (data[0] / maxXAxis); let addr2: number = (this.originY - this.finalY - ((this.originY - this.finalY) % QUYU)) / FOR_VALUE; let y: number = this.originY; this.ctx!.strokeStyle = '#ff0000'; for (let i = 0; i < FOR_VALUE; i++) { this.ctx?.beginPath(); this.ctx?.moveTo(addr1, y); y -= addr2; this.ctx?.lineTo(addr1, y); if (i % 2 !== 0) { this.ctx?.stroke(); } } this.ctx!.font = '10px KATTI'; this.ctx!.fillStyle = '#ff0000'; this.ctx?.fillText( data[1] + 'Hz最大负载线', addr1 - FOR_VALUE / 3, this.originY - addr2 * FOR_VALUE - FOR_VALUE / 4 ); this.ctx!.fillStyle = '#000000'; this.ctx?.fillText('过供给区', addr1 / 2, y + FOR_VALUE / 2); this.ctx?.fillText('欠供给区', addr1 / 2, this.originY - this.finalY); this.ctx?.fillText('超负载区', addr1 + FOR_VALUE / 3, (this.finalY + this.originY) / 2); } /** * 绘制均衡线 */ drawBalanceLine(data: Array): void { let maxXAxis: number = 1; const QUYU: number = 100; const FOR_VALUE = 60; if (this.options) { maxXAxis = this.options.xAxisLabel[this.options.xAxisLabel.length - 1]; } // data[1]用来标注n Hz均衡线 let addr1: number = ((this.finalX - this.originX - ((this.finalX - this.originX) % QUYU)) * (data[0] / maxXAxis)) / FOR_VALUE; let addr2: number = (this.originY - this.finalY - ((this.originY - this.finalY) % QUYU)) / FOR_VALUE; let x: number = this.originX; let y: number = this.originY; this.ctx!.strokeStyle = '#00ff00'; for (let i = 0; i < FOR_VALUE; i++) { this.ctx?.beginPath(); this.ctx?.moveTo(x, y); x += addr1; y -= addr2; this.ctx?.lineTo(x, y); if (i % 2 === 0) { this.ctx?.stroke(); } } this.ctx?.save(); this.ctx?.translate(addr1 * 25 + this.originX, addr2 * 40 + this.finalY); this.ctx!.font = '10px KATTI'; this.ctx!.fillStyle = '#ff0f00'; this.ctx?.rotate(-Math.atan(addr2 / addr1)); this.ctx?.fillText(data[1] + 'Hz均衡线', 0, 0); this.ctx?.restore(); } /*检测是否hover在散点之上*/ checkHover(options: LitChartScatterConfig | undefined, pos: Object): Object | boolean { let data: Array = []; if (options) { data = options.paintingData; } let found: boolean | Object = false; for (let i = 0; i < data.length; i++) { found = false; // @ts-ignore if ( Math.sqrt( // @ts-ignore Math.pow(pos.x - data[i].x, 2) + Math.pow(pos.y - data[i].y, 2) // @ts-ignore ) < data[i].r ) { found = data[i]; break; } } return found; } /*绘制hover状态*/ paintHover(): void { let obj: Object | null = this.options!.hoverData; // @ts-ignore let x: number = obj?.x; // @ts-ignore let y: number = obj?.y; // @ts-ignore let r: number = obj?.r; // @ts-ignore let c: string = obj?.color; let step: number = 0.5; this.ctx!.globalAlpha = 1; this.ctx!.fillStyle = c; for (let i = 0; i < 10; i++) { this.ctx?.beginPath(); this.ctx?.arc(x, y, r + i * step, 0, 2 * Math.PI, false); this.ctx?.fill(); this.ctx?.closePath(); } } //利用离屏canvas恢复hover前的状态 resetHoverWithOffScreen(): void { let obj: Object | null = null; const STEP_VALUE: number = 12; const OUT_CYCLE: number = 2; if (this.options) { obj = this.options.hoverData; } if (!obj) { return; } // @ts-ignore let { x, y, r, c, color } = obj; let step = 0.5; this.ctx!.globalAlpha = 1; for (let i = 10; i > 0; i--) { this.ctx?.save(); //绘制外圆范围 this.ctx?.drawImage( this.canvas2!, x - r - STEP_VALUE * step, y - r - STEP_VALUE * step, OUT_CYCLE * (r + STEP_VALUE * step), OUT_CYCLE * (r + STEP_VALUE * step), x - r - STEP_VALUE * step, y - r - STEP_VALUE * step, OUT_CYCLE * (r + STEP_VALUE * step), OUT_CYCLE * (r + STEP_VALUE * step) ); //绘制内圆 this.ctx?.beginPath(); this.ctx?.arc(x, y, r + i * step, 0, OUT_CYCLE * Math.PI, false); this.ctx?.closePath(); this.ctx!.fillStyle = color; this.ctx!.globalAlpha = 0.8; //填充内圆 this.ctx?.fill(); this.ctx?.restore(); } this.options!.hoverData = null; } /** * 显示提示框 */ showTip(data: any): void { const minWidth: number = 160; const miniHeight: number = 70; const canvasWidth: number = Number(this.canvas?.style.width.replace('px', '')); const canvasHeight: number = Number(this.canvas?.style.height.replace('px', '')); this.scatterTipEL!.style.display = 'flex'; if (canvasWidth - data.x < minWidth && canvasHeight - data.y >= miniHeight) { this.scatterTipEL!.style.top = `${data.y}px`; this.scatterTipEL!.style.left = `${data.x - minWidth}px`; } else if (canvasHeight - data.y < miniHeight && canvasWidth - data.x > minWidth) { this.scatterTipEL!.style.top = `${data.y - miniHeight}px`; this.scatterTipEL!.style.left = `${data.x}px`; } else if (canvasWidth - data.x < minWidth && canvasHeight - data.y < miniHeight) { this.scatterTipEL!.style.top = `${data.y - miniHeight}px`; this.scatterTipEL!.style.left = `${data.x - minWidth}px`; } else { this.scatterTipEL!.style.top = `${data.y}px`; this.scatterTipEL!.style.left = `${data.x}px`; } this.scatterTipEL!.innerHTML = this.options!.tip(data); // @ts-ignore this.options!.hoverEvent('CPU-FREQ', true, data.c[2] - 1); } /** * 隐藏提示框 */ hideTip(): void { this.scatterTipEL!.style.display = 'none'; if (this.options) { // @ts-ignore this.options!.hoverEvent('CPU-FREQ', false); } } connectedCallback(): void { super.connectedCallback(); this.canvas = this.shadowRoot!.querySelector('#canvas'); this.scatterTipEL = this.shadowRoot!.querySelector('#tip'); this.ctx = this.canvas!.getContext('2d', { alpha: true }); this.labelsEL = this.shadowRoot!.querySelector('#shape'); resizeCanvas(this.canvas!); this.originX = this.clientWidth * 0.1; this.originY = this.clientHeight * 0.9; this.finalX = this.clientWidth; this.finalY = this.clientHeight * 0.1; /*hover效果*/ this.canvas!.onmousemove = (event) => { let pos: Object = { x: event.offsetX, y: event.offsetY, }; let hoverPoint: Object | boolean = this.checkHover(this.options, pos); /** * 如果当前有聚焦点 */ if (hoverPoint) { this.showTip(hoverPoint); let samePoint: boolean = this.options!.hoverData === hoverPoint ? true : false; if (!samePoint) { this.resetHoverWithOffScreen(); this.options!.hoverData = hoverPoint; } this.paintHover(); } else { //使用离屏canvas恢复 this.resetHoverWithOffScreen(); this.hideTip(); } }; } initElements(): void { new ResizeObserver((entries, observer) => { entries.forEach((it) => { resizeCanvas(this.canvas!); this.originX = this.clientWidth * 0.1; this.originY = this.clientHeight * 0.95; this.finalX = this.clientWidth * 0.9; this.finalY = this.clientHeight * 0.1; this.labelsEL!.innerText = ''; this.init(); }); }).observe(this); } initHtml(): string { return ( `
`; } }