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.js'; 17import { LitChartColumnConfig } from './LitChartColumnConfig.js'; 18import { resizeCanvas } from '../helper.js'; 19import { getProbablyTime } from '../../../trace/database/logic-worker/ProcedureLogicWorkerCommon.js'; 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 measure() { 197 if (!this.litChartColumnCfg) return; 198 this.data = []; 199 this.rowLines = []; 200 if (!this.litChartColumnCfg.seriesField) { 201 let maxValue = Math.max(...this.litChartColumnCfg.data.map((it) => it[this.litChartColumnCfg!.yField])); 202 maxValue = Math.ceil(maxValue * 0.1) * 10; 203 let partWidth = (this.clientWidth - this.offset!.x!) / this.litChartColumnCfg.data.length; 204 let partHeight = this.clientHeight - this.offset!.y!; 205 let gap = partHeight / 5; 206 let valGap = maxValue / 5; 207 for (let i = 0; i <= 5; i++) { 208 this.rowLines.push({ 209 y: gap * i, 210 label: `${getProbablyTime(maxValue - valGap * i)}`, 211 }); 212 } 213 this.litChartColumnCfg?.data 214 .sort((a, b) => b[this.litChartColumnCfg!.yField] - a[this.litChartColumnCfg!.yField]) 215 .forEach((litChartColumnItem, litChartColumnIndex, array) => { 216 this.data.push({ 217 color: this.litChartColumnCfg!.color(litChartColumnItem), 218 obj: litChartColumnItem, 219 root: true, 220 xLabel: litChartColumnItem[this.litChartColumnCfg!.xField], 221 yLabel: litChartColumnItem[this.litChartColumnCfg!.yField], 222 bgFrame: { 223 x: this.offset!.x! + partWidth * litChartColumnIndex, 224 y: 0, 225 w: partWidth, 226 h: partHeight, 227 }, 228 centerX: this.offset!.x! + partWidth * litChartColumnIndex + partWidth / 2, 229 centerY: 230 partHeight - 231 (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue + 232 (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 2, 233 frame: { 234 x: this.offset!.x! + partWidth * litChartColumnIndex + partWidth / 6, 235 y: partHeight - (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 236 w: partWidth - partWidth / 3, 237 h: (litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 238 }, 239 height: 0, 240 heightStep: Math.ceil((litChartColumnItem[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 60), 241 process: true, 242 }); 243 }); 244 } else { 245 let reduceGroup = this.litChartColumnCfg.data.reduce((pre, current, index, arr) => { 246 (pre[current[this.litChartColumnCfg!.xField]] = pre[current[this.litChartColumnCfg!.xField]] || []).push( 247 current 248 ); 249 return pre; 250 }, {}); 251 let sums = Reflect.ownKeys(reduceGroup).map((k) => 252 (reduceGroup[k] as any[]).reduce((pre, current) => pre + current[this.litChartColumnCfg!.yField], 0) 253 ); 254 let maxValue = Math.ceil(Math.max(...sums) * 0.1) * 10; 255 let partWidth = (this.clientWidth - this.offset!.x!) / Reflect.ownKeys(reduceGroup).length; 256 let partHeight = this.clientHeight - this.offset!.y!; 257 let gap = partHeight / 5; 258 let valGap = maxValue / 5; 259 for (let index = 0; index <= 5; index++) { 260 this.rowLines.push({ 261 y: gap * index, 262 label: `${getProbablyTime(maxValue - valGap * index)} `, 263 }); 264 } 265 Reflect.ownKeys(reduceGroup) 266 .sort( 267 (b, a) => 268 (reduceGroup[a] as any[]).reduce((pre, cur) => pre + (cur[this.litChartColumnCfg!.yField] as number), 0) - 269 (reduceGroup[b] as any[]).reduce((pre, cur) => pre + (cur[this.litChartColumnCfg!.yField] as number), 0) 270 ) 271 .forEach((reduceGroupKey, reduceGroupIndex) => { 272 let elements = reduceGroup[reduceGroupKey]; 273 let initH = 0; 274 elements.forEach((itemEl: any, y: number) => { 275 this.data.push({ 276 color: this.litChartColumnCfg!.color(itemEl), 277 obj: itemEl, 278 root: y == 0, 279 type: itemEl[this.litChartColumnCfg!.seriesField], 280 xLabel: itemEl[this.litChartColumnCfg!.xField], 281 yLabel: itemEl[this.litChartColumnCfg!.yField], 282 bgFrame: { 283 x: this.offset!.x! + partWidth * reduceGroupIndex, 284 y: 0, 285 w: partWidth, 286 h: partHeight, 287 }, 288 centerX: this.offset!.x! + partWidth * reduceGroupIndex + partWidth / 2, 289 centerY: 290 partHeight - 291 initH - 292 (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue + 293 (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 2, 294 frame: { 295 x: this.offset!.x! + partWidth * reduceGroupIndex + partWidth / 6, 296 y: partHeight - (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue - initH, 297 w: partWidth - partWidth / 3, 298 h: (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue, 299 }, 300 height: 0, 301 heightStep: Math.ceil((itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue / 60), 302 process: true, 303 }); 304 initH += (itemEl[this.litChartColumnCfg!.yField] * partHeight) / maxValue; 305 }); 306 }); 307 } 308 } 309 310 get config(): LitChartColumnConfig | null | undefined { 311 return this.litChartColumnCfg; 312 } 313 314 render(ease: boolean = true) { 315 if (!this.litChartColumnCanvas || !this.litChartColumnCfg) return; 316 this.litChartColumnCtx!.clearRect(0, 0, this.clientWidth, this.clientHeight); 317 this.drawLine(this.litChartColumnCtx!); 318 this.data?.forEach((it) => this.drawColumn(this.litChartColumnCtx!, it, ease)); 319 if (ease) { 320 if (this.data.filter((it) => it.process).length > 0) { 321 requestAnimationFrame(() => this.render(ease)); 322 } 323 } 324 } 325 326 drawLine(c: CanvasRenderingContext2D) { 327 c.strokeStyle = '#dfdfdf'; 328 c.lineWidth = 1; 329 c.beginPath(); 330 c.fillStyle = '#8c8c8c'; 331 this.rowLines.forEach((it, i) => { 332 c.moveTo(this.offset!.x!, it.y); 333 c.lineTo(this.clientWidth, it.y); 334 if (i == 0) { 335 c.fillText(it.label, this.offset!.x! - c.measureText(it.label).width - 2, it.y + 11); 336 } else { 337 c.fillText(it.label, this.offset!.x! - c.measureText(it.label).width - 2, it.y + 4); 338 } 339 }); 340 c.stroke(); 341 c.closePath(); 342 } 343 344 drawColumn(c: CanvasRenderingContext2D, it: Pillar, ease: boolean) { 345 if (it.hover) { 346 c.globalAlpha = 0.2; 347 c.fillStyle = '#999999'; 348 c.fillRect(it.bgFrame!.x, it.bgFrame!.y, it.bgFrame!.w, it.bgFrame!.h); 349 c.globalAlpha = 1.0; 350 } 351 c.fillStyle = it.color || '#ff0000'; 352 if (ease) { 353 if (it.height! < it.frame!.h) { 354 it.process = true; 355 c.fillRect(it.frame!.x, it.frame!.y + (it.frame!.h - it.height!), it.frame!.w, it.height!); 356 it.height! += it.heightStep!; 357 } else { 358 c.fillRect(it.frame!.x, it.frame!.y, it.frame!.w, it.frame!.h); 359 it.process = false; 360 } 361 } else { 362 c.fillRect(it.frame!.x, it.frame!.y, it.frame!.w, it.frame!.h); 363 it.process = false; 364 } 365 366 c.beginPath(); 367 c.strokeStyle = '#d8d8d8'; 368 c.moveTo(it.centerX!, it.frame!.y + it.frame!.h!); 369 if (it.root) { 370 c.lineTo(it.centerX!, it.frame!.y + it.frame!.h + 4); 371 } 372 let xMetrics = c.measureText(it.xLabel!); 373 let xMetricsH = xMetrics.actualBoundingBoxAscent + xMetrics.actualBoundingBoxDescent; 374 let yMetrics = c.measureText(it.yLabel!); 375 let yMetricsH = yMetrics.fontBoundingBoxAscent + yMetrics.fontBoundingBoxDescent; 376 c.fillStyle = '#8c8c8c'; 377 if (it.root) { 378 c.fillText(it.xLabel!, it.centerX! - xMetrics.width / 2, it.frame!.y + it.frame!.h + 15); 379 } 380 c.fillStyle = '#fff'; 381 if (this.litChartColumnCfg?.label) { 382 if (yMetricsH < it.frame!.h) { 383 c.fillText( 384 // @ts-ignore 385 this.litChartColumnCfg!.label!.content ? this.litChartColumnCfg!.label!.content(it.obj) : it.yLabel!, 386 it.centerX! - yMetrics.width / 2, 387 it.centerY! + (it.frame!.h - it.height!) / 2 388 ); 389 } 390 } 391 c.stroke(); 392 c.closePath(); 393 } 394 395 beginPath(stroke: boolean, fill: boolean) { 396 return (fn: (c: CanvasRenderingContext2D) => void) => { 397 this.litChartColumnCtx!.beginPath(); 398 fn?.(this.litChartColumnCtx!); 399 if (stroke) { 400 this.litChartColumnCtx!.stroke(); 401 } 402 if (fill) { 403 this.litChartColumnCtx!.fill(); 404 } 405 this.litChartColumnCtx!.closePath(); 406 }; 407 } 408 409 showTip(x: number, y: number, msg: string) { 410 this.litChartColumnTipEL!.style.display = 'flex'; 411 this.litChartColumnTipEL!.style.top = `${y}px`; 412 this.litChartColumnTipEL!.style.left = `${x}px`; 413 this.litChartColumnTipEL!.innerHTML = msg; 414 } 415 416 hideTip() { 417 this.litChartColumnTipEL!.style.display = 'none'; 418 } 419 420 initHtml(): string { 421 return ` 422 <style> 423 :host { 424 display: flex; 425 flex-direction: column; 426 width: 100%; 427 height: 100%; 428 } 429 #tip{ 430 background-color: #f5f5f4; 431 border: 1px solid #fff; 432 border-radius: 5px; 433 color: #333322; 434 font-size: 8pt; 435 position: absolute; 436 min-width: max-content; 437 display: none; 438 top: 0; 439 left: 0; 440 pointer-events: none; 441 user-select: none; 442 padding: 5px 10px; 443 box-shadow: 0 0 10px #22ffffff; 444 /*transition: left;*/ 445 /*transition-duration: 0.3s;*/ 446 } 447 #root{ 448 position:relative; 449 } 450 .tip-content{ 451 display: flex; 452 flex-direction: column; 453 } 454 </style> 455 <div id="root"> 456 <canvas id="canvas"></canvas> 457 <div id="tip"></div> 458 </div>`; 459 } 460} 461 462function contains(rect: { x: number; y: number; w: number; h: number }, x: number, y: number): boolean { 463 return rect.x <= x && x <= rect.x + rect.w && rect.y <= y && y <= rect.y + rect.h; 464} 465