• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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