From e6d8dc8dbf7304f0b16e5ecc097904b2a54bde91 Mon Sep 17 00:00:00 2001 From: CSimoesJr Date: Tue, 6 May 2025 13:13:44 -0300 Subject: [PATCH] feat(charts): implementa chart do tipo pie --- .../po-chart-new/po-chart-grid-utils.ts | 40 ++++++ .../po-chart-new/po-chart-grid.utils.spec.ts | 53 +++++++ .../po-chart-new/po-chart-new.component.html | 8 +- .../po-chart-new.component.spec.ts | 133 ++++++++++++------ .../po-chart-new/po-chart-new.component.ts | 91 ++++++++---- 5 files changed, 251 insertions(+), 74 deletions(-) diff --git a/projects/ui/src/lib/components/po-chart-new/po-chart-grid-utils.ts b/projects/ui/src/lib/components/po-chart-new/po-chart-grid-utils.ts index c123a59b5..51631761e 100644 --- a/projects/ui/src/lib/components/po-chart-new/po-chart-grid-utils.ts +++ b/projects/ui/src/lib/components/po-chart-new/po-chart-grid-utils.ts @@ -28,6 +28,7 @@ export class PoChartGridUtils { fontFamily: this.component.getCSSVariable('--font-family-grid', '.po-chart'), fontSize: tokenFontSizeGrid || 12, fontWeight: Number(this.component.getCSSVariable('--font-weight-grid', '.po-chart')), + color: this.component.getCSSVariable('--color-legend', '.po-chart'), rotate: this.component.options?.axis?.rotateLegend, interval: 0, width: 72, @@ -50,6 +51,7 @@ export class PoChartGridUtils { margin: 10, fontFamily: this.component.getCSSVariable('--font-family-grid', '.po-chart'), fontSize: tokenFontSizeGrid || 12, + color: this.component.getCSSVariable('--color-legend', '.po-chart'), fontWeight: Number(this.component.getCSSVariable('--font-weight-grid', '.po-chart')) }, splitLine: { @@ -134,10 +136,48 @@ export class PoChartGridUtils { color: color }; serie.emphasis = { focus: 'series' }; + serie.blur = { + itemStyle: { opacity: 0.4 } + }; this.component.boundaryGap = true; } } + setListTypePie() { + let radius = '85%'; + let positionHorizontal; + if (this.component.options?.legend === false) { + radius = '95%'; + positionHorizontal = '50%'; + } else { + positionHorizontal = this.component.options?.legendVerticalPosition === 'top' ? '54%' : '46%'; + } + this.component.listTypePie = [ + { + type: 'pie', + center: ['50%', positionHorizontal], + radius: radius, + emphasis: { focus: 'self' }, + data: [], + blur: { itemStyle: { opacity: 0.4 } } + } + ]; + } + + setSerieTypePie(serie: any, color: string) { + if (this.component.listTypePie?.length) { + const borderWidth = this.resolvePx('--border-width-sm'); + const borderColor = this.component.getCSSVariable('--border-color', '.po-chart'); + const seriePie = { + name: serie.name, + value: serie.data, + itemStyle: { borderWidth: borderWidth, borderColor: borderColor, color: color }, + label: { show: false } + }; + this.component.listTypePie[0].data.push(seriePie); + } + } + resolvePx(size: string, selector?: string): number { const token = this.component.getCSSVariable(size, selector); if (token.endsWith('px')) { diff --git a/projects/ui/src/lib/components/po-chart-new/po-chart-grid.utils.spec.ts b/projects/ui/src/lib/components/po-chart-new/po-chart-grid.utils.spec.ts index f7b0836c8..8de56171b 100644 --- a/projects/ui/src/lib/components/po-chart-new/po-chart-grid.utils.spec.ts +++ b/projects/ui/src/lib/components/po-chart-new/po-chart-grid.utils.spec.ts @@ -85,4 +85,57 @@ describe('PoChartGridUtils', () => { expect(option.xAxis['axisLabel'].overflow).toBe('break'); }); }); + + describe('setListPie', () => { + it('should set pie config with radius 95% and center 50% 50% when legend is false', () => { + utils['component'].options = { legend: false } as any; + + utils.setListTypePie(); + + expect(utils['component'].listTypePie).toEqual([ + { + type: 'pie', + center: ['50%', '50%'], + radius: '95%', + emphasis: { focus: 'self' }, + data: [], + blur: { itemStyle: { opacity: 0.4 } } + } + ]); + }); + + it('should set pie config with center 50% 54% when legendVerticalPosition is top', () => { + utils['component'].options = { legend: true, legendVerticalPosition: 'top' } as any; + + utils.setListTypePie(); + + expect(utils['component'].listTypePie).toEqual([ + { + type: 'pie', + center: ['50%', '54%'], + radius: '85%', + emphasis: { focus: 'self' }, + data: [], + blur: { itemStyle: { opacity: 0.4 } } + } + ]); + }); + + it('should set pie config with center 50% 46% when legendVerticalPosition is not top', () => { + utils['component'].options = { legend: true, legendVerticalPosition: 'bottom' } as any; + + utils.setListTypePie(); + + expect(utils['component'].listTypePie).toEqual([ + { + type: 'pie', + center: ['50%', '46%'], + radius: '85%', + emphasis: { focus: 'self' }, + data: [], + blur: { itemStyle: { opacity: 0.4 } } + } + ]); + }); + }); }); diff --git a/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.html b/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.html index 42413894f..875555055 100644 --- a/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.html +++ b/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.html @@ -1,7 +1,11 @@
-
- {{ title }} +
+ {{ title }}
diff --git a/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.spec.ts b/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.spec.ts index b461f3eb4..681cd63ab 100644 --- a/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.spec.ts +++ b/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.spec.ts @@ -122,6 +122,29 @@ describe('PoChartNewComponent', () => { expect(component).toBeTruthy(); }); + it('should show tooltip only when content overflows', () => { + const event: any = { + target: document.createElement('div') + }; + + const element = event.target as HTMLElement; + + // Simula overflow + Object.defineProperty(element, 'scrollWidth', { value: 150, configurable: true }); + Object.defineProperty(element, 'offsetWidth', { value: 100, configurable: true }); + + component.title = 'Long Title'; + component.showTooltipTitle(event); + expect(component['tooltipTitle']).toBe('Long Title'); + + // Simula conteúdo sem overflow + Object.defineProperty(element, 'scrollWidth', { value: 80 }); + Object.defineProperty(element, 'offsetWidth', { value: 100 }); + + component.showTooltipTitle(event); + expect(component['tooltipTitle']).toBeUndefined(); + }); + describe('Lifecycle hooks:', () => { it('ngAfterViewInit: should initialize echarts', () => { spyOn(component, 'initECharts'); @@ -399,6 +422,35 @@ describe('PoChartNewComponent', () => { expect(component.seriesHover.emit).not.toHaveBeenCalled(); }); + it('should emit seriesClick event when clicking on the chart if params.seriesName is undefined', () => { + component['chartInstance'] = { + on: jasmine.createSpy('on') + } as any; + + spyOn(component.seriesClick, 'emit'); + spyOn(component.seriesHover, 'emit'); + + component['initEChartsEvents'](); + + expect(component['chartInstance'].on).toHaveBeenCalledWith('click', jasmine.any(Function)); + + const clickCallback = component['chartInstance'].on.calls.argsFor(0)[1]; + + const mockParams = { value: 100, name: 'Name X' }; + clickCallback(mockParams); + + const mouseoverCallback = component['chartInstance'].on.calls.argsFor(1)[1]; + + const mockParamsMouse = {}; + mouseoverCallback(mockParamsMouse); + + expect(component.seriesClick.emit).toHaveBeenCalledWith({ + label: 'Name X', + data: 100 + }); + expect(component.seriesHover.emit).not.toHaveBeenCalled(); + }); + it('should emit seriesHover event when hovering over a series', () => { const tooltipElement = document.createElement('div'); tooltipElement.id = 'custom-tooltip'; @@ -465,7 +517,7 @@ describe('PoChartNewComponent', () => { on: jasmine.createSpy('on') } as any; - spyOn(component.poTooltip, 'toggleTooltipVisibility'); + spyOn(component.poTooltip.last, 'toggleTooltipVisibility'); component['initEChartsEvents'](); @@ -475,7 +527,7 @@ describe('PoChartNewComponent', () => { mouseoutCallback(); - expect(component.poTooltip.toggleTooltipVisibility).toHaveBeenCalledWith(false); + expect(component.poTooltip.last.toggleTooltipVisibility).toHaveBeenCalledWith(false); }); it('should set tooltipText as "seriesName: value" when tooltip is not defined', () => { @@ -524,7 +576,7 @@ describe('PoChartNewComponent', () => { mouseoverCallback(mockParamsNoSeriesName); - expect(component.tooltipText.replace(/\s/g, '')).toBe('CategoriaSemNome99'.replace(/\s/g, '')); + expect(component.tooltipText.replace(/\s/g, '')).toBe('CategoriaSemNome:99'.replace(/\s/g, '')); }); }); @@ -735,9 +787,11 @@ describe('PoChartNewComponent', () => { const result = component['setSeries'](); - expect(result.length).toBe(2); + expect(result.length).toBe(1); expect(result[0].type).toBe('pie'); - expect(result[0].name).toBe('Serie 1'); + expect(result[0].data[0].name).toBe('Serie 1'); + expect(result[0].data[1].name).toBe('Serie 2'); + expect(result[0].data.length).toBe(2); }); it('should transform series correctly with default configurations', () => { @@ -1013,6 +1067,33 @@ describe('PoChartNewComponent', () => { expect(component['setTableColumns']).not.toHaveBeenCalled(); }); + + it('should set Series if type is Pie', () => { + component.type = PoChartType.Pie; + component.series = [ + { data: 80, label: 'Pie Value 1' }, + { data: 20, label: 'Pie Value 2' } + ]; + component['chartInstance'] = { + getOption: jasmine.createSpy('getOption').and.returnValue({ + series: [ + { + name: 'Série A', + data: [ + { name: 'Pie Value 1', value: 80 }, + { name: 'Pie Value 2', value: 20 } + ] + } + ] + }) + } as any; + spyOn(component as any, 'setTableColumns'); + + component['setTableProperties'](); + + expect(component['setTableColumns']).not.toHaveBeenCalled(); + expect(component['itemsTable']).toEqual([{ 'Série': '-', 'Pie Value 1': 80, 'Pie Value 2': 20 }]); + }); }); describe('setTableColumns:', () => { @@ -1055,6 +1136,7 @@ describe('PoChartNewComponent', () => { { serie: 'Série 1', valor1: 10, valor2: undefined }, { serie: 'Série 2', valor1: 30 } ]; + component['columnsTable'] = [{ property: 'serie' }, { property: 'valor1' }, { property: 'valor2' }]; component.options = {} as any; component['downloadCsv'](); @@ -1192,47 +1274,6 @@ describe('PoChartNewComponent', () => { expect(component['setHeaderProperties']).not.toHaveBeenCalled(); }); - // it('should create and download a PNG image correctly', done => { - // const chartElement = document.createElement('div'); - // chartElement.style.width = '800px'; - // chartElement.style.height = '600px'; - - // const headerElement = document.createElement('div'); - // headerElement.style.height = '50px'; - - // const mockImage = new Image(); - // const canvas = document.createElement('canvas'); - // const ctx = canvas.getContext('2d'); - // spyOn(canvas, 'getContext').and.returnValue(ctx); - - // const link = document.createElement('a'); - // spyOn(document, 'createElement').and.callFake((tag: string) => { - // if (tag === 'canvas') return canvas; - // if (tag === 'a') return link; - // return document.createElement(tag); - // }); - - // spyOn(canvas, 'toDataURL').and.returnValue('data:image/png;base64,fakeImageData'); - // spyOn(link, 'click'); - - // component['configureImageCanvas']('png', mockImage); - - // setTimeout(() => { - // mockImage.onload?.(new Event('load')); - // }, 100); - - // setTimeout(() => { - // try { - // expect(link.href).toBe('data:image/png;base64,fakeImageData'); - // expect(link.download).toBe('grafico-exportado.png'); - // expect(link.click).toHaveBeenCalled(); - // done(); - // } catch (error) { - // done.fail(error); - // } - // }, 300); - // }); - it('should create and download a PNG image correctly', done => { const chartElement = document.createElement('div'); chartElement.style.width = '800px'; diff --git a/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.ts b/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.ts index 5fede54c6..d17aa35aa 100644 --- a/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.ts +++ b/projects/ui/src/lib/components/po-chart-new/po-chart-new.component.ts @@ -6,8 +6,10 @@ import { HostListener, OnChanges, OnInit, + QueryList, SimpleChanges, - ViewChild + ViewChild, + ViewChildren } from '@angular/core'; import { CurrencyPipe, DecimalPipe } from '@angular/common'; @@ -57,7 +59,7 @@ import { PoChartGridUtils } from './po-chart-grid-utils'; standalone: false }) export class PoChartNewComponent extends PoChartNewBaseComponent implements OnInit, AfterViewInit, OnChanges { - @ViewChild(PoTooltipDirective) poTooltip: PoTooltipDirective; + @ViewChildren(PoTooltipDirective) poTooltip: QueryList; @ViewChild('targetPopup', { read: ElementRef, static: false }) targetRef: ElementRef; @ViewChild('modalComponent') modal: PoModalComponent; @@ -67,6 +69,7 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn chartMarginTop = '0px'; isTypeBar = false; boundaryGap = false; + listTypePie: Array; protected actionModal: PoModalAction = { action: this.downloadCsv.bind(this), label: this.literals.downloadCSV @@ -76,7 +79,8 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn protected isExpanded = false; protected legendData: Array<{ name: string; color: string }> = []; protected headerHeight: number; - protected positionTooltip = 'bottom'; + protected positionTooltip = 'top'; + protected tooltipTitle = undefined; protected chartGridUtils: PoChartGridUtils; protected popupActions: Array = [ { @@ -158,6 +162,16 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn this.initECharts(); } + showTooltipTitle(e: MouseEvent) { + const element = e.target as HTMLElement; + + if (element.offsetWidth < element.scrollWidth) { + this.tooltipTitle = this.title; + } else { + this.tooltipTitle = undefined; + } + } + getCSSVariable(variable: string, selector?: string): string { const element = selector ? document.querySelector(selector) : document.documentElement; return element ? getComputedStyle(element).getPropertyValue(variable).trim() : ''; @@ -211,7 +225,11 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn private initEChartsEvents() { this.chartInstance.on('click', params => { if (!params.value) return; - this.seriesClick.emit({ label: params.seriesName, data: params.value, category: params.name }); + if (params.seriesName && !params.seriesName.includes('\u00000')) { + this.seriesClick.emit({ label: params.seriesName, data: params.value, category: params.name }); + } else { + this.seriesClick.emit({ label: params.name, data: params.value }); + } }); this.chartInstance.on('mouseover', (params: any) => { @@ -220,26 +238,28 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn const divTooltipElement = this.el.nativeElement.querySelector('#custom-tooltip'); if (divTooltipElement) { const chartElement = this.el.nativeElement.querySelector('#chart-id'); - if (params.seriesType === 'bar') { - this.positionTooltip = 'top'; - } - const customTooltipText = params.seriesName - ? `${params.name}
+ const customTooltipText = + params.seriesName && !params.seriesName.includes('\u00000') + ? `${params.name}
${params.seriesName}: ${params.value}` - : `${params.name}${params.value}`; + : `${params.name}: ${params.value}`; this.tooltipText = this.series[params.seriesIndex].tooltip ? this.series[params.seriesIndex].tooltip : customTooltipText; divTooltipElement.style.left = `${params.event.offsetX + chartElement.offsetLeft + 3}px`; divTooltipElement.style.top = `${chartElement.offsetTop + params.event.offsetY + 3}px`; - this.poTooltip.toggleTooltipVisibility(true); + this.poTooltip.last.toggleTooltipVisibility(true); } } - this.seriesHover.emit({ label: params.seriesName, data: params.value, category: params.name }); + if (params.seriesName && !params.seriesName.includes('\u00000')) { + this.seriesHover.emit({ label: params.seriesName, data: params.value, category: params.name }); + } else { + this.seriesHover.emit({ label: params.name, data: params.value }); + } }); this.chartInstance.on('mouseout', () => { - this.poTooltip.toggleTooltipVisibility(false); + this.poTooltip.last.toggleTooltipVisibility(false); }); } @@ -257,13 +277,14 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn series: newSeries as any }; - this.chartGridUtils.setGridOption(options); - this.chartGridUtils.setOptionsAxis(options); - this.formatLabelOption(options); - this.chartGridUtils.setShowAxisDetails(options); - - if (this.options?.dataZoom) { - this.chartGridUtils.setOptionDataZoom(options); + if (!this.listTypePie?.length) { + this.chartGridUtils.setGridOption(options); + this.chartGridUtils.setOptionsAxis(options); + this.formatLabelOption(options); + this.chartGridUtils.setShowAxisDetails(options); + if (this.options?.dataZoom) { + this.chartGridUtils.setOptionDataZoom(options); + } } if (this.options?.legend !== false) { @@ -309,13 +330,16 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn const hasArea = this.type === 'area' || this.series.some(serie => serie.type === 'area'); const newSeries: Array = [...this.colorService.getColors(this.series, true, hasArea)]; const tokenBorderWidthMd = this.chartGridUtils.resolvePx('--border-width-md'); - const findType = this.series.find(serie => serie.type); + const findType = this.series.find(serie => serie.type)?.type; let typeDefault; if (!findType && !this.type) { typeDefault = Array.isArray(this.series[0].data) ? PoChartType.Column : PoChartType.Pie; } + if (findType === 'pie' || typeDefault === 'pie' || this.type === 'pie') { + this.chartGridUtils.setListTypePie(); + } - return newSeries.map((serie, index) => { + const seriesUpdated = newSeries.map((serie, index) => { serie.name = serie.label && typeof serie.label === 'string' ? serie.label : ''; !serie.type ? this.setTypeSerie(serie, this.type || typeDefault) : this.setTypeSerie(serie, serie.type); @@ -323,6 +347,7 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn ? this.getCSSVariable(`--${serie.color.replace('po-', '')}`) : serie.color; + this.chartGridUtils.setSerieTypePie(serie, colorVariable); this.setSerieEmphasis(serie, colorVariable, tokenBorderWidthMd); this.chartGridUtils.setSerieTypeLine(serie, tokenBorderWidthMd, colorVariable); this.chartGridUtils.setSerieTypeArea(serie, index); @@ -330,6 +355,11 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn return serie; }); + + if (this.listTypePie?.length) { + return this.listTypePie; + } + return seriesUpdated; } private setSerieEmphasis(serie, color: string, tokenBorder: number) { @@ -385,10 +415,17 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn private setTableProperties() { const option = this.chartInstance.getOption(); - let categories: Array = this.isTypeBar ? option.yAxis[0].data : option.xAxis[0].data; - if (!categories && Array.isArray(this.series[0]?.data)) { + let categories: Array = this.isTypeBar ? option.yAxis[0].data : option.xAxis?.[0].data; + if (!categories) { categories = []; - this.series[0].data.forEach((data, index) => categories.push(String(index))); + if (Array.isArray(this.series[0]?.data)) { + this.series[0].data.forEach((data, index) => categories.push(String(index))); + } else { + let items = { [this.options?.firstColumnName || 'Série']: '-' }; + option.series[0].data.forEach(data => (items = { ...items, [data.name]: data.value })); + this.itemsTable = [items]; + return; + } } const series: any = option.series; @@ -443,7 +480,9 @@ export class PoChartNewComponent extends PoChartNewBaseComponent implements OnIn const headers = Object.keys(this.itemsTable[0]); const columnNameDefault = this.isTypeBar ? 'Categoria' : 'Série'; const firstColumnName = this.options?.firstColumnName || columnNameDefault; - const orderedHeaders = [firstColumnName, ...headers.filter(header => header !== 'serie')]; + const orderedHeaders = this.columnsTable?.length + ? [firstColumnName, ...headers.filter(header => header !== 'serie')] + : [...headers.filter(header => header !== 'serie')]; const csvContent = [ orderedHeaders.join(';'),