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 { BaseElement, element } from '../../BaseElement'; 17import { LitChartColumnConfig } from './LitChartColumnConfig'; 18import { resizeCanvas } from '../helper'; 19import { getProbablyTime } from '../../../trace/database/logic-worker/ProcedureLogicWorkerCommon'; 20 21class Pillar { 22 obj?: any; 23 xLabel?: string; 24 yLabel?: string; 25 type?: string; 26 root?: boolean; 27 bgFrame?: { 28 x: number; 29 y: number; 30 w: number; 31 h: number; 32 }; 33 frame?: { 34 x: number; 35 y: number; 36 w: number; 37 h: number; 38 }; 39 height?: number; 40 process?: boolean; 41 heightStep?: number; 42 centerX?: number; 43 centerY?: number; 44 color?: string; 45 hover?: boolean; 46} 47 48interface RLine { 49 label: string; 50 y: number; 51} 52 53@element('lit-chart-column') 54export class LitChartColumn extends BaseElement { 55 private litChartColumnTipEL: HTMLDivElement | null | undefined; 56 litChartColumnCanvas: HTMLCanvasElement | undefined | null; 57 litChartColumnCtx: CanvasRenderingContext2D | undefined | null; 58 litChartColumnCfg: LitChartColumnConfig | null | undefined; 59 offset?: { x: number | undefined; y: number | undefined }; 60 data: Pillar[] = []; 61 rowLines: RLine[] = []; 62 63 connectedCallback() { 64 super.connectedCallback(); 65 this.litChartColumnTipEL = this.shadowRoot!.querySelector<HTMLDivElement>('#tip'); 66 this.litChartColumnCanvas = this.shadowRoot!.querySelector<HTMLCanvasElement>('#canvas'); 67 this.litChartColumnCtx = this.litChartColumnCanvas!.getContext('2d', { alpha: true }); 68 resizeCanvas(this.litChartColumnCanvas!); 69 this.offset = { x: 60, y: 20 }; 70 this.litChartColumnCanvas!.onmouseout = (e) => { 71 this.hideTip(); 72 this.data.forEach((it) => (it.hover = false)); 73 this.render(); 74 }; 75 this.litChartColumnCanvas!.onmousemove = (ev) => { 76 let rect = this.getBoundingClientRect(); 77 let x = ev.pageX - rect.left; 78 let y = ev.pageY - rect.top; 79 this.data.forEach((it) => { 80 if (contains(it.bgFrame!, x, y)) { 81 it.hover = true; 82 this.litChartColumnCfg?.hoverHandler?.(it.obj.no); 83 } else { 84 it.hover = false; 85 } 86 }); 87 let pillars = this.data.filter((it) => it.hover); 88 if (this.litChartColumnCfg?.seriesField) { 89 if (pillars.length > 0) { 90 let titleEl = `<label>${this.litChartColumnCfg.xField}: ${pillars[0].xLabel}</label>`; 91 let messageEl = pillars.map((it) => `<label>${it.type}: ${it.yLabel}</label>`).join(''); 92 let sumEl = `<label>Total: ${pillars 93 .map((item) => item.obj[this.litChartColumnCfg?.yField!]) 94 .reduce((pre, current) => pre + current, 0)}</label>`; 95 let innerHtml = `<div class="tip-content">${titleEl}${messageEl}${sumEl}</div>`; 96 this.tipTypeShow(x, y, pillars, innerHtml); 97 } 98 } else { 99 if (pillars.length > 0) { 100 let title = `<label>${pillars[0].xLabel}:${pillars[0].yLabel}</label>`; 101 let innerHtml = `<div class="tip-content">${title}</div>`; 102 this.tipTypeShow(x, y, pillars, innerHtml); 103 } 104 } 105 106 if (this.data.filter((it) => it.process).length == 0) { 107 this.render(); 108 } 109 }; 110 this.render(); 111 } 112 113 private tipTypeShow(x: number, y: number, pillars: Pillar[], innerHtml: string) { 114 if (x >= this.clientWidth - this.litChartColumnTipEL!.clientWidth) { 115 this.showTip( 116 x - this.litChartColumnTipEL!.clientWidth - 10, 117 y - 20, 118 this.litChartColumnCfg!.tip ? this.litChartColumnCfg!.tip(pillars) : innerHtml 119 ); 120 } else { 121 this.showTip(x + 10, y - 20, this.litChartColumnCfg!.tip ? this.litChartColumnCfg!.tip(pillars) : innerHtml); 122 } 123 } 124 125 showHoverColumn(index: number) { 126 this.data.forEach((it) => { 127 if (it.obj.no === index) { 128 it.hover = true; 129 } else { 130 it.hover = false; 131 } 132 }); 133 let pillars = this.data.filter((it) => it.hover); 134 if (this.litChartColumnCfg?.seriesField) { 135 if (pillars.length > 0) { 136 let hoverData = pillars[0]; 137 let title = `<label>${this.litChartColumnCfg.xField}: ${pillars[0].xLabel}</label>`; 138 let msg = pillars.map((it) => `<label>${it.type}: ${it.yLabel}</label>`).join(''); 139 let sum = `<label>Total: ${pillars 140 .map((it) => it.obj[this.litChartColumnCfg?.yField!]) 141 .reduce((pre, current) => pre + current, 0)}</label>`; 142 let innerHtml = `<div class="tip-content">${title}${msg}${sum}</div>`; 143 this.showTip( 144 this.clientWidth / 2, 145 this.clientHeight / 2, 146 this.litChartColumnCfg!.tip ? this.litChartColumnCfg!.tip(pillars) : innerHtml 147 ); 148 } 149 } else { 150 if (pillars.length > 0) { 151 let hoverData = pillars[0]; 152 let title = `<label>${pillars[0].xLabel}:${pillars[0].yLabel}</label>`; 153 let innerHtml = `<div class="tip-content">${title}</div>`; 154 this.showTip( 155 this.clientWidth / 2, 156 this.clientHeight / 2, 157 this.litChartColumnCfg!.tip ? this.litChartColumnCfg!.tip(pillars) : innerHtml 158 ); 159 } 160 } 161 162 if (this.data.filter((it) => it.process).length == 0) { 163 this.render(); 164 } 165 } 166 167 initElements(): void { 168 new ResizeObserver((entries, observer) => { 169 entries.forEach((it) => { 170 resizeCanvas(this.litChartColumnCanvas!); 171 this.measure(); 172 this.render(false); 173 }); 174 }).observe(this); 175 } 176 177 set config(litChartColumnConfig: LitChartColumnConfig | null | undefined) { 178 if (!litChartColumnConfig) return; 179 this.litChartColumnCfg = litChartColumnConfig; 180 this.measure(); 181 this.render(); 182 } 183 184 set dataSource(litChartColumnArr: any[]) { 185 if (this.litChartColumnCfg) { 186 this.litChartColumnCfg.data = litChartColumnArr; 187 this.measure(); 188 this.render(); 189 } 190 } 191 192 get dataSource() { 193 return this.litChartColumnCfg?.data || []; 194 } 195 196 dataSort():void{ 197 if (!this.litChartColumnCfg!.notSort) { 198 this.litChartColumnCfg?.data.sort( 199 (a, b) => b[this.litChartColumnCfg!.yField] - a[this.litChartColumnCfg!.yField] 200 ); 201 } 202 } 203 204 haveSeriesField():void{ 205 let maxValue = Math.max(...this.litChartColumnCfg!.data.map((it) => it[this.litChartColumnCfg!.yField])); 206 maxValue = Math.ceil(maxValue * 0.1) * 10; 207 let partWidth = (this.clientWidth - this.offset!.x!) / this.litChartColumnCfg!.data.length; 208 let partHeight = this.clientHeight - this.offset!.y!; 209 let gap = partHeight / 5; 210 let valGap = maxValue / 5; 211 for (let i = 0; i <= 5; i++) { 212 this.rowLines.push({ 213 y: gap * i, 214 label: 215 this.litChartColumnCfg!.removeUnit === true 216 ? `${maxValue - valGap * i}` 217 : `${getProbablyTime(maxValue - valGap * i)}`, 218 }); 219 } 220 this.dataSort(); 221 this.litChartColumnCfg?.data.forEach((litChartColumnItem, litChartColumnIndex, array) => { 222 this.data.push({ 223 color: this.litChartColumnCfg!.color(litChartColumnItem), 224 obj: litChartColumnItem, 225 root: true, 226 xLabel: litChartColumnItem[this.litChartColumnCfg!.xField], 227 yLabel: litChartColumnItem[this.litChartColumnCfg!.yField], 228 bgFrame: { 229 x: this.offset!.x! + partWidth * litChartColumnIndex, 230 y: 0, 231 w: partWidth, 232 h: partHeight, 233 }, 234 centerX: this.offset!.x! + partWidth * litChartColumnIndex + partWidth / 2, 235 centerY: 236 partHeight - 237 (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue + 238 (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 2, 239 frame: { 240 x: this.offset!.x! + partWidth * litChartColumnIndex + partWidth / 6, 241 y: partHeight - (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 242 w: partWidth - partWidth / 3, 243 h: (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 244 }, 245 height: 0, 246 heightStep: Math.ceil((litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 60), 247 process: true, 248 }); 249 }); 250 } 251 252 noSeriesField(itemEl:any,y:number,initH:number,maxValue:number,partWidth:number,partHeight:number,reduceGroupIndex:number):void{ 253 this.data.push({ 254 color: this.litChartColumnCfg!.color(itemEl), 255 obj: itemEl, 256 root: y === 0, 257 type: itemEl[this.litChartColumnCfg!.seriesField], 258 xLabel: itemEl[this.litChartColumnCfg!.xField], 259 yLabel: itemEl[this.litChartColumnCfg!.yField], 260 bgFrame: { 261 x: this.offset!.x! + partWidth * reduceGroupIndex, 262 y: 0, 263 w: partWidth, 264 h: partHeight, 265 }, 266 centerX: this.offset!.x! + partWidth * reduceGroupIndex + partWidth / 2, 267 centerY: 268 partHeight - 269 initH - 270 (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue + 271 (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 2, 272 frame: { 273 x: this.offset!.x! + partWidth * reduceGroupIndex + partWidth / 6, 274 y: partHeight - (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue - initH, 275 w: partWidth - partWidth / 3, 276 h: (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 277 }, 278 height: 0, 279 heightStep: Math.ceil((itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 60), 280 process: true, 281 }); 282 } 283 284 measure() { 285 if (!this.litChartColumnCfg) return; 286 this.data = []; 287 this.rowLines = []; 288 if (!this.litChartColumnCfg.seriesField) { 289 this.haveSeriesField(); 290 } else { 291 let reduceGroup = this.litChartColumnCfg!.data.reduce((pre, current, index, arr) => { 292 (pre[current[this.litChartColumnCfg!.xField]] = pre[current[this.litChartColumnCfg!.xField]] || []).push( 293 current 294 ); 295 return pre; 296 }, {}); 297 let sums = Reflect.ownKeys(reduceGroup).map((k) => 298 (reduceGroup[k] as any[]).reduce((pre, current) => pre + current[this.litChartColumnCfg!.yField], 0) 299 ); 300 let maxValue = Math.ceil(Math.max(...sums) * 0.1) * 10; 301 let partWidth = (this.clientWidth - this.offset!.x!) / Reflect.ownKeys(reduceGroup).length; 302 let partHeight = this.clientHeight - this.offset!.y!; 303 let gap = partHeight / 5; 304 let valGap = maxValue / 5; 305 for (let index = 0; index <= 5; index++) { 306 this.rowLines.push({ 307 y: gap * index, 308 label: `${getProbablyTime(maxValue - valGap * index)} `, 309 }); 310 } 311 Reflect.ownKeys(reduceGroup) 312 .sort( 313 (b, a) => 314 (reduceGroup[a] as any[]).reduce((pre, cur) => pre + (cur[this.litChartColumnCfg!.yField] as number), 0) - 315 (reduceGroup[b] as any[]).reduce((pre, cur) => pre + (cur[this.litChartColumnCfg!.yField] as number), 0) 316 ) 317 .forEach((reduceGroupKey, reduceGroupIndex) => { 318 let elements = reduceGroup[reduceGroupKey]; 319 let initH = 0; 320 elements.forEach((itemEl: any, y: number) => { 321 this.noSeriesField(itemEl,y,initH,maxValue,partWidth,partHeight,reduceGroupIndex); 322 initH += (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue; 323 }); 324 }); 325 } 326 } 327 328 get config(): LitChartColumnConfig | null | undefined { 329 return this.litChartColumnCfg; 330 } 331 332 render(ease: boolean = true) { 333 if (!this.litChartColumnCanvas || !this.litChartColumnCfg) return; 334 this.litChartColumnCtx!.clearRect(0, 0, this.clientWidth, this.clientHeight); 335 this.drawLine(this.litChartColumnCtx!); 336 this.data?.forEach((it) => this.drawColumn(this.litChartColumnCtx!, it, ease)); 337 if (ease) { 338 if (this.data.filter((it) => it.process).length > 0) { 339 requestAnimationFrame(() => this.render(ease)); 340 } 341 } 342 } 343 344 drawLine(c: CanvasRenderingContext2D) { 345 c.strokeStyle = '#dfdfdf'; 346 c.lineWidth = 1; 347 c.beginPath(); 348 c.fillStyle = '#8c8c8c'; 349 this.rowLines.forEach((it, i) => { 350 c.moveTo(this.offset!.x!, it.y); 351 c.lineTo(this.clientWidth, it.y); 352 if (i == 0) { 353 c.fillText(it.label, this.offset!.x! - c.measureText(it.label).width - 2, it.y + 11); 354 } else { 355 c.fillText(it.label, this.offset!.x! - c.measureText(it.label).width - 2, it.y + 4); 356 } 357 }); 358 c.stroke(); 359 c.closePath(); 360 } 361 362 drawColumn(c: CanvasRenderingContext2D, it: Pillar, ease: boolean) { 363 if (it.hover) { 364 c.globalAlpha = 0.2; 365 c.fillStyle = '#999999'; 366 c.fillRect(it.bgFrame!.x, it.bgFrame!.y, it.bgFrame!.w, it.bgFrame!.h); 367 c.globalAlpha = 1.0; 368 } 369 c.fillStyle = it.color || '#ff0000'; 370 if (ease) { 371 if (it.height! < it.frame!.h) { 372 it.process = true; 373 c.fillRect(it.frame!.x, it.frame!.y + (it.frame!.h - it.height!), it.frame!.w, it.height!); 374 it.height! += it.heightStep!; 375 } else { 376 c.fillRect(it.frame!.x, it.frame!.y, it.frame!.w, it.frame!.h); 377 it.process = false; 378 } 379 } else { 380 c.fillRect(it.frame!.x, it.frame!.y, it.frame!.w, it.frame!.h); 381 it.process = false; 382 } 383 384 c.beginPath(); 385 c.strokeStyle = '#d8d8d8'; 386 c.moveTo(it.centerX!, it.frame!.y + it.frame!.h!); 387 if (it.root) { 388 c.lineTo(it.centerX!, it.frame!.y + it.frame!.h + 4); 389 } 390 let xMetrics = c.measureText(it.xLabel!); 391 let xMetricsH = xMetrics.actualBoundingBoxAscent + xMetrics.actualBoundingBoxDescent; 392 let yMetrics = c.measureText(it.yLabel!); 393 let yMetricsH = yMetrics.fontBoundingBoxAscent + yMetrics.fontBoundingBoxDescent; 394 c.fillStyle = '#8c8c8c'; 395 if (it.root) { 396 c.fillText(it.xLabel!, it.centerX! - xMetrics.width / 2, it.frame!.y + it.frame!.h + 15); 397 } 398 c.fillStyle = '#fff'; 399 if (this.litChartColumnCfg?.label) { 400 if (yMetricsH < it.frame!.h) { 401 c.fillText( 402 // @ts-ignore 403 this.litChartColumnCfg!.label!.content ? this.litChartColumnCfg!.label!.content(it.obj) : it.yLabel!, 404 it.centerX! - yMetrics.width / 2, 405 it.centerY! + (it.frame!.h - it.height!) / 2 406 ); 407 } 408 } 409 c.stroke(); 410 c.closePath(); 411 } 412 413 beginPath(stroke: boolean, fill: boolean) { 414 return (fn: (c: CanvasRenderingContext2D) => void) => { 415 this.litChartColumnCtx!.beginPath(); 416 fn?.(this.litChartColumnCtx!); 417 if (stroke) { 418 this.litChartColumnCtx!.stroke(); 419 } 420 if (fill) { 421 this.litChartColumnCtx!.fill(); 422 } 423 this.litChartColumnCtx!.closePath(); 424 }; 425 } 426 427 showTip(x: number, y: number, msg: string) { 428 this.litChartColumnTipEL!.style.display = 'flex'; 429 this.litChartColumnTipEL!.style.top = `${y}px`; 430 this.litChartColumnTipEL!.style.left = `${x}px`; 431 this.litChartColumnTipEL!.innerHTML = msg; 432 } 433 434 hideTip() { 435 this.litChartColumnTipEL!.style.display = 'none'; 436 } 437 438 initHtml(): string { 439 return ` 440 <style> 441 :host { 442 display: flex; 443 flex-direction: column; 444 width: 100%; 445 height: 100%; 446 } 447 #tip{ 448 background-color: #f5f5f4; 449 border: 1px solid #fff; 450 border-radius: 5px; 451 color: #333322; 452 font-size: 8pt; 453 position: absolute; 454 min-width: max-content; 455 display: none; 456 top: 0; 457 left: 0; 458 pointer-events: none; 459 user-select: none; 460 padding: 5px 10px; 461 box-shadow: 0 0 10px #22ffffff; 462 /*transition: left;*/ 463 /*transition-duration: 0.3s;*/ 464 } 465 #root{ 466 position:relative; 467 } 468 .tip-content{ 469 display: flex; 470 flex-direction: column; 471 } 472 </style> 473 <div id="root"> 474 <canvas id="canvas"></canvas> 475 <div id="tip"></div> 476 </div>`; 477 } 478} 479 480function contains(rect: { x: number; y: number; w: number; h: number }, x: number, y: number): boolean { 481 return rect.x <= x && x <= rect.x + rect.w && rect.y <= y && y <= rect.y + rect.h; 482} 483