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