• 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';
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