Docs›Componentes (Admin)›mad-chart
Componentes (Admin)

mad-chart

Gráfico ECharts via builder PHP.

Componente Blade que renderiza um gráfico ECharts a partir de um objeto MadChartConfig montado em PHP pelo builder fluente Mad\Chart\MadChart. Use-o apenas quando a lógica do gráfico não cabe nas props declarativas do <mad-db-chart> (ver db-chart.md) — por exemplo múltiplas séries com regras de negócio distintas, ou transformação pesada antes de plotar.

Para o caso comum (um model, um agrupamento, uma agregação) prefira sempre <mad-db-chart> — 100% declarativo, zero PHP no controller.

Props do <mad-chart>

Prop Tipo Default Descrição
config MadChart|MadChartConfig — Obrigatório. Builder fluente ou config já construído
class string '' Classes CSS extras no wrapper
height int null Sobrescreve a altura (px) do config
title string null Sobrescreve o título do config
click-action string '' Nome do método do host chamado ao clicar numa fatia/barra (drill-down)

config aceita tanto uma instância de Mad\Chart\MadChart (lazy — toConfig() é chamado em tempo de render) quanto um Mad\Chart\MadChartConfig já pronto.

Builder Mad\Chart\MadChart — API fluente

use Mad\Chart\MadChart;

$chart = MadChart::bar('vendas_mes')
    ->fromModel('PedidoVenda')
    ->groupBy(['mes'])
    ->sum('valor_total')
    ->title('Vendas por Mês')
    ->currency()
    ->height(350);

No Blade:

<mad-chart :config="$chart" />

Factories (tipo do gráfico)

MadChart::bar('nome')
MadChart::line('nome')
MadChart::pie('nome')
MadChart::donut('nome')
MadChart::rose('nome')
MadChart::funnel('nome')
MadChart::treemap('nome')
MadChart::radar('nome')
MadChart::mixed('nome')

'nome' é um identificador único do gráfico na página (vira id do container JS).

radar e mixed são os dois tipos novos (2026-07) — ver as seções Radar e Mixed abaixo.

Fonte de dados

Método Descrição
fromModel(string $class) Classe do model Eloquent fonte dos dados
fromQuery($query) Eloquent/Query Builder pronto — agrega direto sobre o builder (alternativa a fromModel)
database(string $db) Conexão (quando não vier do builder/model)
joins(array $joins) Joins extras aplicados à query de agregação
groupBy(string|array $fields) Campo(s) de agrupamento (eixo X / categorias)
data(array $data) Dados manuais ['Label' => valor, ...] — dispensa fromModel/fromQuery

Agregação

->count()              // COUNT(*)
->sum('valor_total')   // SUM(campo)
->avg('nota')          // AVG(campo)
->max('preco')         // MAX(campo)
->min('preco')         // MIN(campo)

Visual

Método Descrição
title(string $title, string $subtitle = '') Título e subtítulo do card
height(int $px) / width(string $w) Dimensões
noPanel() Renderiza sem o card wrapper (bare chart)
legend(bool $show = true, string $position = 'bottom') Legenda (bottom ou right)
colors(array|string $colors) Paleta custom (array ou CSV)
percentage() Mostra percentuais (pie/donut/rose)

Formatação numérica

->currency(2, 'R$ ', ',', '.')   // precision, prefix, decimal, thousand
->numeric(2, ',', '.')           // sem prefixo
->suffix('%')
->abbreviate()                    // valores abreviados (K, M, B)

Específico por tipo

->stacked()       // bar: empilha séries
->horizontal()    // bar: orientação horizontal
->area(true)      // line: área preenchida (smooth opcional)

Transformers (label/valor)

->transformer(callable $fn)          // transforma o valor antes de plotar
->legendTransformer(callable $fn)    // transforma o label da legenda (ex: id → nome)
->subLegendTransformer(callable $fn) // transforma o sub-label

legendTransformer(callable $fn) aceita qualquer callable — closure ou first-class callable. O motor o chama 1× por valor de legenda. A regra prática não é "closure é proibida": é não fazer query no banco por valor (N+1). Feche sobre um mapa pré-carregado, ou use um Transformers::nome(...) que faz batch (também mais reutilizável e testável):

$chart = MadChart::donut('status')
    ->fromModel('PedidoVenda')
    ->groupBy('status_id')
    ->count()
    ->title('Por Status')
    ->legendTransformer(Transformers::statusPedidoNome(...));

Opções ECharts extras

->options(['grid' => ['left' => 40]])  // merge raso na option final do ECharts

Radar (eixos radiais)

MadChart::radar() transforma as categorias (1ª dimensão do groupBy) nos eixos (indicators) e cada valor da 2ª dimensão num polígono/série.

// ORM — 1 polígono por canal, eixos = meses
$chart = MadChart::radar('vendas_canal')
    ->fromModel('Venda')
    ->groupBy(['mes', 'canal'])
    ->sum('valor')
    ->legend()
    ->title('Vendas por canal');

// Dados manuais — 1 polígono só
$chart = MadChart::radar('perfil')
    ->data(['Frontend' => 8, 'Backend' => 6, 'DevOps' => 4, 'Mobile' => 3]);
Modelo de dados Vira no radar
1ª dimensão do groupBy radar.indicator (os eixos/pontas)
2ª dimensão do groupBy (opcional) Uma série = um polígono (com legenda)
Valor agregado Vértice do polígono naquele eixo
Combinação categoria×série ausente 0 (vetores sempre alinhados aos indicators)

Regras automáticas da engine (nada a configurar):

  • Max único global em todos os indicators, arredondado para um teto "bonito" com folga de ~5% (30 → 40; 730 → 800) — mantém os polígonos comparáveis.
  • Negativos são clampados em 0 (o radar do ECharts não tem eixo negativo); com transformer o tooltip ainda mostra o valor bruto.
  • Tudo zero → max = 1 (evita radar quebrado).
  • Ordem das séries: alfabética — colors() posicionais funcionam.
  • legendTransformer() formata os NOMES dos indicators; subLegendTransformer() formata os nomes das séries (2ª dimensão).

Limitações do radar:

  • Sem drill-through declarativo: filter-prop/filter-mode do <mad-db-chart> são ignorados (o clique devolve o polígono inteiro, não uma categoria). click-action continua permitido, com essa semântica.
  • stacked(), horizontal(), area() não se aplicam.
  • Recomendado ≥ 3 categorias — com 1 ou 2 o polígono degenera em ponto/linha.
  • legend() só tem efeito com groupBy de 2 dimensões.

Mixed (colunas + linha)

MadChart::mixed() desenha colunas e linha(s) no mesmo gráfico, com eixo Y secundário opcional. Tem dois modos mutuamente exclusivos — configurar os dois lança exception.

Modo Quando usar groupBy Método
A — séries do group-by As séries já existem nos dados (2ª dimensão) e algumas devem virar linha 2 dimensões seriesAsLine()
B — 2ª métrica Colunas = uma agregação, linha = OUTRA agregação dos mesmos registros 1 dimensão lineMetric()

Modo A — seriesAsLine()

MadChart::mixed('vendas')
    ->fromQuery($query)
    ->groupBy(['mes', 'canal'])
    ->sum('valor')
    ->stacked()                                   // empilha SÓ as colunas
    ->seriesAsLine(['Meta'], secondaryAxis: true);

// aceita CSV:            ->seriesAsLine('Meta,Média')
// aceita índice inteiro: ->seriesAsLine([1])  (posição na ordem alfabética)

Assinatura: seriesAsLine(array|string $series, bool $secondaryAxis = false).

Match da série: nome case-insensitive contra o nome final da série (já passado pelo subLegendTransformer), OU índice int. Nome sem match é no-op silencioso — o gráfico renderiza como bar puro.

Modo B — lineMetric()

A 2ª agregação roda no mesmo SELECT (uma query só, alias line_total):

MadChart::mixed('acessos')
    ->fromQuery($query)
    ->groupBy('mes')
    ->sum('valor')
    ->lineMetric('avg', 'ticket', 'Ticket médio', secondaryAxis: true);

// count(*) sem campo:
MadChart::mixed('logins')->fromQuery($q)->groupBy('dia')->count()
    ->lineMetric('count');

Assinatura: lineMetric(string $total, ?string $field = null, ?string $label = null, bool $secondaryAxis = false).

Regras:

  • Exige groupBy de 1 dimensão — com 2 dimensões lança exception ("line-metric exige group-by de 1 dimensão; use line-series").
  • $total ∈ count|sum|avg|min|max (mesma allowlist da agregação principal).
  • $field vazio só é válido com count (vira count(*)). count($campo) conta não-nulos — útil para "quantos têm X preenchido".
  • $label default é "{total}({field})" — sempre informe um label legível.
  • $field passa pelo mesmo guard anti-SQL-injection do campo de agregação (OrderGuard::validateExpression).
  • A série de colunas (single-series não tem nome) recebe como name o título do gráfico; sobrescreva com ->options(['series' => [0 => ['name' => 'Logins']]]).

Para o equivalente declarativo (type="radar" / type="mixed", line-series, line-total, line-field, line-label, line-secondary-axis), veja <mad-db-chart>.

Exemplo completo — múltiplas séries com regra de negócio

// Controller / MadComponent
public function chartReceitaPorFilial(): \Mad\Chart\MadChart
{
    return \Mad\Chart\MadChart::bar('receita_filial')
        ->fromQuery(
            \App\Models\PedidoVenda::query()
                ->where('estado_id', '=', 8)
                ->whereNotNull('faturado_em')
        )
        ->groupBy('filial')
        ->sum('valor_total')
        ->title('Receita faturada por filial')
        ->currency()
        ->horizontal()
        ->height(320);
}
<mad-chart :config="$that->chartReceitaPorFilial()" />

Dados manuais (sem model)

$chart = MadChart::pie('distribuicao')
    ->data(['Eletrônicos' => 4500, 'Roupas' => 2100, 'Alimentos' => 3200])
    ->title('Distribuição')
    ->legend();
<mad-chart :config="$chart" />

Sem card wrapper (bare)

$chart = MadChart::line('acessos')
    ->fromModel('Acesso')
    ->groupBy('dia')
    ->count()
    ->title('Acessos Diários')
    ->noPanel()
    ->height(220);
<mad-chart :config="$chart" />

Click-to-filter (drill-down) manual

<mad-chart :config="$chart" click-action="onFatiaClicada" />
public function onFatiaClicada(string $label, $value, int $index): void
{
    // chamado via MadWire ao clicar numa categoria/fatia
}

<mad-db-chart> já resolve esse fluxo de forma declarativa (filter-prop, filter-mode) — use click-action aqui só quando precisar de lógica 100% custom no servidor.

SEMPRE preferir <mad-db-chart> quando der

{{-- ERRADO: usar o builder PHP pra um caso simples que o auto-query resolve --}}
@php
    $chart = \Mad\Chart\MadChart::bar('vendas')
        ->fromModel('PedidoVenda')
        ->groupBy('mes')
        ->sum('valor_total')
        ->title('Vendas')
        ->currency();
@endphp
<mad-chart :config="$chart" />

{{-- CERTO: declarativo no Blade, zero PHP no controller --}}
<mad-db-chart type="bar" model="PedidoVenda"
    group-by="mes" field="valor_total" total="sum"
    title="Vendas" format="currency:R$" />
{{-- ERRADO: montar gráfico ECharts manualmente com <script> --}}
<div id="meu-chart" style="height:300px;"></div>
<script>
    var c = echarts.init(document.getElementById('meu-chart'));
    c.setOption({ ... });
</script>

{{-- CERTO: builder PHP + componente --}}
<mad-chart :config="$chart" />
// ERRADO: query no banco por valor de legenda (N+1) — um find() por fatia
$chart->legendTransformer(fn($v) => \App\Models\StatusPedido::find($v)?->nome ?? $v);

// CERTO: closure sobre um mapa pré-carregado (1 query)
$statusMap = \App\Models\StatusPedido::pluck('nome', 'id')->all();
$chart->legendTransformer(fn($v) => $statusMap[$v] ?? $v);

// CERTO (alternativa): Transformers global que faz batch internamente
$chart->legendTransformer(Transformers::statusPedidoNome(...));

Quando usar cada um

Preciso... Usar
Um model, um agrupamento, uma agregação simples <mad-db-chart> (ver db-chart.md)
Múltiplas séries / regra de negócio condicional / fromQuery custom pesado MadChart builder + <mad-chart :config="...">
Dados já calculados em memória (sem ORM) MadChart::pie(...)->data([...]) ou <mad-db-chart :data="[...]">