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?: unknown; 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(): void { 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): void => { 71 this.hideTip(); 72 this.data.forEach((it) => (it.hover = false)); 73 this.render(); 74 }; 75 this.litChartColumnCanvas!.onmousemove = (ev): void => { 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; //@ts-ignore 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 //@ts-ignore 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): void { 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): void { 126 this.data.forEach((it) => { 127 //@ts-ignore 128 if (it.obj.no === index) { 129 it.hover = true; 130 } else { 131 it.hover = false; 132 } 133 }); 134 let pillars = this.data.filter((it) => it.hover); 135 if (this.litChartColumnCfg?.seriesField) { 136 if (pillars.length > 0) { 137 let hoverData = pillars[0]; 138 let title = `<label>${this.litChartColumnCfg.xField}: ${pillars[0].xLabel}</label>`; 139 let msg = pillars.map((it) => `<label>${it.type}: ${it.yLabel}</label>`).join(''); 140 let sum = `<label>Total: ${pillars //@ts-ignore 141 .map((it) => it.obj[this.litChartColumnCfg?.yField!]) 142 .reduce((pre, current) => pre + current, 0)}</label>`; 143 let innerHtml = `<div class="tip-content">${title}${msg}${sum}</div>`; 144 this.showTip( 145 this.clientWidth / 2, 146 this.clientHeight / 2, 147 this.litChartColumnCfg!.tip ? this.litChartColumnCfg!.tip(pillars) : innerHtml 148 ); 149 } 150 } else { 151 if (pillars.length > 0) { 152 let hoverData = pillars[0]; 153 let title = `<label>${pillars[0].xLabel}:${pillars[0].yLabel}</label>`; 154 let innerHtml = `<div class="tip-content">${title}</div>`; 155 this.showTip( 156 this.clientWidth / 2, 157 this.clientHeight / 2, 158 this.litChartColumnCfg!.tip ? this.litChartColumnCfg!.tip(pillars) : innerHtml 159 ); 160 } 161 } 162 163 if (this.data.filter((it) => it.process).length === 0) { 164 this.render(); 165 } 166 } 167 168 initElements(): void { 169 new ResizeObserver((entries, observer) => { 170 entries.forEach((it) => { 171 resizeCanvas(this.litChartColumnCanvas!); 172 this.measure(); 173 this.render(false); 174 }); 175 }).observe(this); 176 } 177 178 set config(litChartColumnConfig: LitChartColumnConfig | null | undefined) { 179 if (!litChartColumnConfig) { 180 return; 181 } 182 this.litChartColumnCfg = litChartColumnConfig; 183 this.measure(); 184 this.render(); 185 } 186 187 set dataSource(litChartColumnArr: unknown[]) { 188 if (this.litChartColumnCfg) { 189 this.litChartColumnCfg.data = litChartColumnArr; 190 this.measure(); 191 this.render(); 192 } 193 } 194 195 get dataSource(): unknown[] { 196 return this.litChartColumnCfg?.data || []; 197 } 198 199 dataSort(): void { 200 if (!this.litChartColumnCfg!.notSort) { 201 this.litChartColumnCfg?.data.sort( 202 //@ts-ignore 203 (a, b) => b[this.litChartColumnCfg!.yField] - a[this.litChartColumnCfg!.yField] 204 ); 205 } 206 } 207 208 haveSeriesField(): void { 209 //@ts-ignore 210 let maxValue = Math.max(...this.litChartColumnCfg!.data.map((it) => it[this.litChartColumnCfg!.yField])); 211 maxValue = Math.ceil(maxValue * 0.1) * 10; 212 let partWidth = (this.clientWidth - this.offset!.x!) / this.litChartColumnCfg!.data.length; 213 let partHeight = this.clientHeight - this.offset!.y!; 214 let gap = partHeight / 5; 215 let valGap = maxValue / 5; 216 for (let i = 0; i <= 5; i++) { 217 this.rowLines.push({ 218 y: gap * i, 219 label: 220 this.litChartColumnCfg!.removeUnit === true 221 ? `${maxValue - valGap * i}` 222 : `${getProbablyTime(maxValue - valGap * i)}`, 223 }); 224 } 225 this.dataSort(); 226 this.litChartColumnCfg?.data.forEach((litChartColumnItem, litChartColumnIndex, array) => { 227 this.data.push({ 228 color: this.litChartColumnCfg!.color(litChartColumnItem), 229 obj: litChartColumnItem, 230 root: true, //@ts-ignore 231 xLabel: litChartColumnItem[this.litChartColumnCfg!.xField], //@ts-ignore 232 yLabel: litChartColumnItem[this.litChartColumnCfg!.yField], 233 bgFrame: { 234 x: this.offset!.x! + partWidth * litChartColumnIndex, 235 y: 0, 236 w: partWidth, 237 h: partHeight, 238 }, 239 centerX: this.offset!.x! + partWidth * litChartColumnIndex + partWidth / 2, 240 centerY: 241 partHeight - //@ts-ignore 242 (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue + //@ts-ignore 243 (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 2, 244 frame: { 245 x: this.offset!.x! + partWidth * litChartColumnIndex + partWidth / 6, //@ts-ignore 246 y: partHeight - (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 247 w: partWidth - partWidth / 3, //@ts-ignore 248 h: (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 249 }, 250 height: 0, //@ts-ignore 251 heightStep: Math.ceil((litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 60), 252 process: true, 253 }); 254 }); 255 } 256 257 noSeriesField( 258 itemEl: unknown, 259 y: number, 260 initH: number, 261 maxValue: number, 262 partWidth: number, 263 partHeight: number, 264 reduceGroupIndex: number 265 ): void { 266 this.data.push({ 267 color: this.litChartColumnCfg!.color(itemEl), 268 obj: itemEl, 269 root: y === 0, //@ts-ignore 270 type: itemEl[this.litChartColumnCfg!.seriesField], //@ts-ignore 271 xLabel: itemEl[this.litChartColumnCfg!.xField], //@ts-ignore 272 yLabel: itemEl[this.litChartColumnCfg!.yField], 273 bgFrame: { 274 x: this.offset!.x! + partWidth * reduceGroupIndex, 275 y: 0, 276 w: partWidth, 277 h: partHeight, 278 }, 279 centerX: this.offset!.x! + partWidth * reduceGroupIndex + partWidth / 2, 280 centerY: 281 partHeight - 282 initH - //@ts-ignore 283 (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue + //@ts-ignore 284 (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 2, 285 frame: { 286 x: this.offset!.x! + partWidth * reduceGroupIndex + partWidth / 6, //@ts-ignore 287 y: partHeight - (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue - initH, 288 w: partWidth - partWidth / 3, //@ts-ignore 289 h: (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 290 }, 291 height: 0, //@ts-ignore 292 heightStep: Math.ceil((itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 60), 293 process: true, 294 }); 295 } 296 297 measure(): void { 298 if (!this.litChartColumnCfg) { 299 return; 300 } 301 this.data = []; 302 this.rowLines = []; 303 if (!this.litChartColumnCfg.seriesField) { 304 this.haveSeriesField(); 305 } else { 306 let reduceGroup = this.litChartColumnCfg!.data.reduce((pre, current, index, arr) => { 307 //@ts-ignore 308 (pre[current[this.litChartColumnCfg!.xField]] = pre[current[this.litChartColumnCfg!.xField]] || []).push( 309 current 310 ); 311 return pre; 312 }, {}); //@ts-ignore 313 let sums = Reflect.ownKeys(reduceGroup).map( 314 ( 315 k //@ts-ignore 316 ) => (reduceGroup[k] as unknown[]).reduce((pre, current) => pre + current[this.litChartColumnCfg!.yField], 0) 317 ); //@ts-ignore 318 let maxValue = Math.ceil(Math.max(...sums) * 0.1) * 10; //@ts-ignore 319 let partWidth = (this.clientWidth - this.offset!.x!) / Reflect.ownKeys(reduceGroup).length; 320 let partHeight = this.clientHeight - this.offset!.y!; 321 let gap = partHeight / 5; 322 let valGap = maxValue / 5; 323 for (let index = 0; index <= 5; index++) { 324 this.rowLines.push({ 325 y: gap * index, 326 label: `${getProbablyTime(maxValue - valGap * index)} `, 327 }); 328 } //@ts-ignore 329 Reflect.ownKeys(reduceGroup) 330 .sort( 331 (b, a) =>//@ts-ignore 332 (reduceGroup[a] as unknown[]).reduce((pre, cur) => pre + (cur[this.litChartColumnCfg!.yField] as number), 0) -//@ts-ignore 333 (reduceGroup[b] as unknown[]).reduce((pre, cur) => pre + (cur[this.litChartColumnCfg!.yField] as number), 0) 334 ) 335 .forEach((reduceGroupKey, reduceGroupIndex) => { 336 //@ts-ignore 337 let elements = reduceGroup[reduceGroupKey]; 338 let initH = 0; 339 elements.forEach((itemEl: unknown, y: number) => { 340 this.noSeriesField(itemEl, y, initH, maxValue, partWidth, partHeight, reduceGroupIndex); //@ts-ignore 341 initH += (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue; 342 }); 343 }); 344 } 345 } 346 347 get config(): LitChartColumnConfig | null | undefined { 348 return this.litChartColumnCfg; 349 } 350 351 render(ease: boolean = true): void { 352 if (!this.litChartColumnCanvas || !this.litChartColumnCfg) { 353 return; 354 } 355 this.litChartColumnCtx!.clearRect(0, 0, this.clientWidth, this.clientHeight); 356 this.drawLine(this.litChartColumnCtx!); 357 this.data?.forEach((it) => this.drawColumn(this.litChartColumnCtx!, it, ease)); 358 if (ease) { 359 if (this.data.filter((it) => it.process).length > 0) { 360 requestAnimationFrame(() => this.render(ease)); 361 } 362 } 363 } 364 365 drawLine(c: CanvasRenderingContext2D): void { 366 c.strokeStyle = '#dfdfdf'; 367 c.lineWidth = 1; 368 c.beginPath(); 369 c.fillStyle = '#8c8c8c'; 370 this.rowLines.forEach((it, i) => { 371 c.moveTo(this.offset!.x!, it.y); 372 c.lineTo(this.clientWidth, it.y); 373 if (i === 0) { 374 c.fillText(it.label, this.offset!.x! - c.measureText(it.label).width - 2, it.y + 11); 375 } else { 376 c.fillText(it.label, this.offset!.x! - c.measureText(it.label).width - 2, it.y + 4); 377 } 378 }); 379 c.stroke(); 380 c.closePath(); 381 } 382 383 drawColumn(c: CanvasRenderingContext2D, it: Pillar, ease: boolean): void { 384 if (it.hover) { 385 c.globalAlpha = 0.2; 386 c.fillStyle = '#999999'; 387 c.fillRect(it.bgFrame!.x, it.bgFrame!.y, it.bgFrame!.w, it.bgFrame!.h); 388 c.globalAlpha = 1.0; 389 } 390 c.fillStyle = it.color || '#ff0000'; 391 if (ease) { 392 if (it.height! < it.frame!.h) { 393 it.process = true; 394 c.fillRect(it.frame!.x, it.frame!.y + (it.frame!.h - it.height!), it.frame!.w, it.height!); 395 it.height! += it.heightStep!; 396 } else { 397 c.fillRect(it.frame!.x, it.frame!.y, it.frame!.w, it.frame!.h); 398 it.process = false; 399 } 400 } else { 401 c.fillRect(it.frame!.x, it.frame!.y, it.frame!.w, it.frame!.h); 402 it.process = false; 403 } 404 405 c.beginPath(); 406 c.strokeStyle = '#d8d8d8'; 407 c.moveTo(it.centerX!, it.frame!.y + it.frame!.h!); 408 if (it.root) { 409 c.lineTo(it.centerX!, it.frame!.y + it.frame!.h + 4); 410 } 411 let xMetrics = c.measureText(it.xLabel!); 412 let xMetricsH = xMetrics.actualBoundingBoxAscent + xMetrics.actualBoundingBoxDescent; 413 let yMetrics = c.measureText(it.yLabel!); 414 let yMetricsH = yMetrics.fontBoundingBoxAscent + yMetrics.fontBoundingBoxDescent; 415 c.fillStyle = '#8c8c8c'; 416 if (it.root) { 417 c.fillText(it.xLabel!, it.centerX! - xMetrics.width / 2, it.frame!.y + it.frame!.h + 15); 418 } 419 c.fillStyle = '#fff'; 420 if (this.litChartColumnCfg?.label) { 421 if (yMetricsH < it.frame!.h) { 422 c.fillText( 423 // @ts-ignore 424 this.litChartColumnCfg!.label!.content ? this.litChartColumnCfg!.label!.content(it.obj) : it.yLabel!, 425 it.centerX! - yMetrics.width / 2, 426 it.centerY! + (it.frame!.h - it.height!) / 2 427 ); 428 } 429 } 430 c.stroke(); 431 c.closePath(); 432 } 433 434 beginPath(stroke: boolean, fill: boolean): (fn: (c: CanvasRenderingContext2D) => void) => void { 435 return (fn: (c: CanvasRenderingContext2D) => void) => { 436 this.litChartColumnCtx!.beginPath(); 437 fn?.(this.litChartColumnCtx!); 438 if (stroke) { 439 this.litChartColumnCtx!.stroke(); 440 } 441 if (fill) { 442 this.litChartColumnCtx!.fill(); 443 } 444 this.litChartColumnCtx!.closePath(); 445 }; 446 } 447 448 showTip(x: number, y: number, msg: string): void { 449 this.litChartColumnTipEL!.style.display = 'flex'; 450 this.litChartColumnTipEL!.style.top = `${y}px`; 451 this.litChartColumnTipEL!.style.left = `${x}px`; 452 this.litChartColumnTipEL!.innerHTML = msg; 453 } 454 455 hideTip(): void { 456 this.litChartColumnTipEL!.style.display = 'none'; 457 } 458 459 initHtml(): string { 460 return ` 461 <style> 462 :host { 463 display: flex; 464 flex-direction: column; 465 width: 100%; 466 height: 100%; 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 min-width: max-content; 476 display: none; 477 top: 0; 478 left: 0; 479 pointer-events: none; 480 user-select: none; 481 padding: 5px 10px; 482 box-shadow: 0 0 10px #22ffffff; 483 /*transition: left;*/ 484 /*transition-duration: 0.3s;*/ 485 } 486 #root{ 487 position:relative; 488 } 489 .tip-content{ 490 display: flex; 491 flex-direction: column; 492 } 493 </style> 494 <div id="root"> 495 <canvas id="canvas"></canvas> 496 <div id="tip"></div> 497 </div>`; 498 } 499} 500 501function contains(rect: { x: number; y: number; w: number; h: number }, x: number, y: number): boolean { 502 return rect.x <= x && x <= rect.x + rect.w && rect.y <= y && y <= rect.y + rect.h; 503} 504