1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

Show Tooltip on legend hover in Google Charts

Discussão em 'Angular' iniciado por IgorGarciaRez, Agosto 27, 2025.

  1. i was wondering how can i show the tooltip on a piechart in Google Charts when the cursor hovers over the legends. The hover already activates a visual selection of the slice, but the tooltip isnt activated. When hovering over the slice, it works normally.

    visual selection of the slice on hover

    Here is my code:

    html

    <!---Pie charts ---->
    <div class="br-card py-3 mt-4 text-center">

    <div class="d-flex flex-row justify-content-center">
    <div style="flex:1;">
    <google-chart [type]="pieChartImoveis.type" [data]="pieChartImoveis.data"
    [options]="pieChartImoveis.options" [width]="chartWidth/2" [height]="300"
    (select)="onPieSelect($event, 'imoveis')" (ready)="onChartReady($event, 'imoveis')">
    </google-chart>
    </div>
    <div style="flex:1;">
    <google-chart [type]="pieChartArea.type" [data]="pieChartArea.data" [options]="pieChartArea.options"
    [width]="chartWidth/2" [height]="300" (select)="onPieSelect($event, 'area')" (ready)="onChartReady($event, 'area')">
    </google-chart>
    </div>
    </div>

    </div>


    typescript

    declare const google: any;
    import { Component, ViewChild, ViewChildren, ElementRef, QueryList, AfterViewInit, HostListener, ChangeDetectorRef } from '@angular/core';
    import { GoogleChart, ChartType, ChartSelectionChangedEvent } from 'angular-google-charts';
    import { MapaComponent } from 'src/app/components/mapa/mapa.component';
    import { jsPDF } from 'jspdf';
    import autoTable from 'jspdf-autotable';

    @Component({
    selector: 'app-terras-indigenas',
    templateUrl: './terras-indigenas.component.html',
    styleUrl: './terras-indigenas.component.css',
    standalone: true,
    imports: [GoogleChart, MapaComponent]
    })
    export class TerrasIndigenasPageComponent implements AfterViewInit {

    public ChartType = ChartType;

    @ViewChild('graficoContainer', { read: ElementRef }) graficoContainer!: ElementRef;
    @ViewChild(MapaComponent) mapa!: MapaComponent;
    @ViewChildren(GoogleChart) charts!: QueryList<GoogleChart>;

    chartWidth = 100;
    selectedPorte: Porte | null = null;

    pieChartImoveis = {
    type: ChartType.PieChart,
    data: [
    ['Pequeno', 87],
    ['Grande', 22],
    ['Médio', 10]
    ],
    options: {
    title: 'Tamanho do imóvel: Imóveis com sobreposição em TI',
    fontName: 'Raleway',
    fontSize: 14,
    titleTextStyle: {
    fontSize: 16,
    bold: true
    },
    colors: ['#ffcd07', '#155bcb', '#168821'],
    backgroundColor: 'transparent',
    legend: {
    position: 'right',
    alignment: 'top'
    },
    pieSliceText: 'percentage',
    pieSliceTextStyle: {
    color: 'black',
    fontSize: 14,
    bold: true
    },
    pieHole: 0.3,
    is3D: false,
    tooltip: { trigger: 'both' }
    }
    };

    pieChartArea = {
    type: ChartType.PieChart,
    title: 'Tamanho do imóvel: Área com sobreposição em TI',
    data: [
    ['Pequeno', 88168.761],
    ['Grande', 3275.394],
    ['Médio', 266.482]
    ],
    options: {
    title: 'Tamanho do imóvel: Área com sobreposição em TI',
    fontName: 'Raleway',
    fontSize: 14,
    titleTextStyle: {
    fontSize: 16,
    bold: true
    },
    colors: ['#ffcd07', '#155bcb', '#168821'],
    backgroundColor: 'transparent',
    legend: {
    position: 'right',
    alignment: 'top'
    },
    pieSliceText: 'percentage',
    pieSliceTextStyle: {
    color: 'black',
    fontSize: 14,
    bold: true
    },
    pieHole: 0.3,
    is3D: false,
    tooltip: { trigger: 'both' }
    }
    };
    constructor(private cdr: ChangeDetectorRef) { }

    @HostListener('window:resize', ['$event'])
    onResize(event?: Event) {
    this.setChartWidth();
    }

    ngAfterViewInit(): void {
    this.setChartWidth();
    this.cdr.detectChanges();

    }


    setChartWidth(): void {
    if (this.graficoContainer?.nativeElement) {
    this.chartWidth = this.graficoContainer.nativeElement.offsetWidth;
    }
    }

    onPieSelect(event: ChartSelectionChangedEvent | any, fonte: 'imoveis' | 'area') {
    const row =
    event?.selection?.[0]?.row ??
    (typeof event?.row === 'number' ? event.row : null);

    if (row == null) return;

    const data = fonte === 'imoveis' ? this.pieChartImoveis.data : this.pieChartArea.data;
    const label = (data[row]?.[0] as Porte) || null;

    if (!label) return;
    // toggle - se clicar no mesmo porte, limpa o filtro
    this.selectedPorte = this.selectedPorte === label ? null : label;
    this.applyPorteFilter();
    }

    clearPorteFilter() {
    this.selectedPorte = null;
    this.applyPorteFilter();
    }

    private applyPorteFilter() {
    // atualiza a metrica de tamanho + dados
    const novaDataCombo = this.selectedPorte ? this.comboByPorte[this.selectedPorte] : this.comboAll;
    const novaDataFaixa = this.selectedPorte ? this.faixaByPorte[this.selectedPorte] : this.faixaAll;
    const novaDataComboTiSobre = this.selectedPorte ? this.comboTiSobreByPorte[this.selectedPorte] : this.comboTiSobreAll;
    //cria novo array
    this.imoveisSobreTiChart = {
    ...this.imoveisSobreTiChart, data: [...novaDataCombo]
    };
    this.faixaSobreTiChart = {
    ...this.faixaSobreTiChart, data: [...novaDataFaixa]
    };
    this.tiSobreposicaoFunaiChart = {
    ...this.tiSobreposicaoFunaiChart, data: [...novaDataComboTiSobre]
    };
    // aplica destaque para a fatia escolhida
    const applySliceHighlight = (pie: any) => {
    const idx = pie.data.findIndex((r: any[]) => r[0] === this.selectedPorte);
    const slices: any = {};
    pie.data.forEach((r: any[], i: number) => {
    slices = i === idx && this.selectedPorte != null ? { offset: 0.08 } : {};
    });
    pie.options = { ...pie.options, slices };
    };
    applySliceHighlight(this.pieChartImoveis);
    applySliceHighlight(this.pieChartArea);
    }

    exportarTabelaCsv() {
    const colunas = this.tabelaDeSintese.columnNames;
    const linhas = this.tabelaDeSintese.data;

    let csvContent = colunas.join(";") + "\n";
    linhas.forEach(row => {
    csvContent += row.join(";") + "\n";
    });

    const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.setAttribute("href", url);
    link.setAttribute("download", "tabela_sintese.csv");
    link.style.visibility = "hidden";
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    }

    private async fitImage(
    dataUrl: string,
    maxWmm: number,
    maxHmm: number,
    pdf: jsPDF
    ): Promise<{ w: number; h: number }> {
    const img = new Image();
    img.src = dataUrl;
    await img.decode();
    const pxToMm = (px: number) => px * 25.4 / 96; // 96dpi
    const iw = pxToMm(img.width), ih = pxToMm(img.height);
    const ratio = Math.min(maxWmm / iw, maxHmm / ih);
    return { w: iw * ratio, h: ih * ratio };
    }

    async criarPdf(): Promise<void> {
    const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
    const pageW = pdf.internal.pageSize.getWidth();
    const pageH = pdf.internal.pageSize.getHeight();
    const margin = 10;
    let y = margin;

    pdf.setFontSize(16);
    pdf.text('Imóveis x Terras Indígenas — Relatório', margin, y);
    y += 8;

    const mapUrl = await this.mapa.capturePng();
    const mapSize = await this.fitImage(mapUrl, pageW - 2 * margin, 90, pdf);
    pdf.addImage(mapUrl, 'PNG', margin, y, mapSize.w, mapSize.h);
    y += mapSize.h + 6;

    const chartUris: string[] = [];
    this.charts.forEach((c) => {
    try {
    const chart = (c as any).wrapper?.getChart?.();
    if (chart?.getImageURI) chartUris.push(chart.getImageURI());
    } catch { }
    });

    const gap = 5;
    const colW = (pageW - 2 * margin - gap) / 2;
    const targetH = 70;

    for (let i = 0; i < chartUris.length; i++) {
    const isLeft = i % 2 === 0;
    if (isLeft && (y + targetH > pageH - 50)) {
    pdf.addPage(); y = margin;
    }
    const x = isLeft ? margin : margin + colW + gap;
    pdf.addImage(chartUris, 'PNG', x, y, colW, targetH);
    if (!isLeft) y += targetH + 6;
    }
    if (chartUris.length % 2 === 1) { y += targetH + 6; }

    if (y + 30 > pageH - 20) { pdf.addPage(); y = margin; }
    pdf.setFontSize(14);
    pdf.text(this.tabelaDeSintese.options.title, margin, y);
    y += 6;

    autoTable(pdf, {
    startY: y,
    head: [this.tabelaDeSintese.columnNames],
    body: this.tabelaDeSintese.data,
    styles: { fontSize: 9, cellPadding: 2 },
    headStyles: { fillColor: [21, 91, 203], textColor: 255, halign: 'center' },
    theme: 'grid',
    margin: { left: margin, right: margin }
    });

    pdf.save('mapa-painel.pdf');
    }

    onChartReady(event: any, fonte: 'imoveis' | 'area') {
    const chartWrapper = event.chartWrapper;
    if (!chartWrapper) return;

    const chart = chartWrapper.getChart();
    if (!chart) return;

    google.visualization.events.removeAllListeners(chart);

    google.visualization.events.addListener(chart, 'onmouseover', (e: any) => {
    chart.setSelection([{ row: e.row }]);
    });

    google.visualization.events.addListener(chart, 'onmouseout', () => {
    chart.setSelection([]);
    });

    google.visualization.events.addListener(chartWrapper, 'select', () => {
    const sel = chartWrapper.getChart().getSelection();
    if (sel.length) {
    const valorSelecionado = fonte === 'imoveis'
    ? this.pieChartImoveis.data[sel[0].row][0]
    : this.pieChartArea.data[sel[0].row][0];
    this.selectedPorte = valorSelecionado as Porte;
    this.applyPorteFilter();
    }
    });
    }
    }

    Continue reading...

Compartilhe esta Página