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
transformero 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-modedo<mad-db-chart>são ignorados (o clique devolve o polígono inteiro, não uma categoria).click-actioncontinua 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 comgroupByde 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
groupByde 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).$fieldvazio só é válido comcount(viracount(*)).count($campo)conta não-nulos — útil para "quantos têm X preenchido".$labeldefault é"{total}({field})"— sempre informe um label legível.$fieldpassa 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
nameo 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="[...]"> |