mad-db-chart
Gráfico ECharts auto-query (bar/line/pie/donut/funnel/treemap).
Componente Blade auto-contido que monta um gráfico ECharts a partir de props HTML — sem PHP no controller.
Props
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| type | string | 'bar' | bar, line, pie, donut, rose, funnel, treemap, radar, mixed |
| name | string | auto | ID único do chart (auto-gerado quando omitido) |
| model | string | '' | Classe do model Eloquent (ex: PedidoVenda) |
| database | string | MAIN_DATABASE | Conexão do banco |
| group-by | string/array | '' | Campo(s) de agrupamento (máx. 2 — 2º vira sub-série) |
| field | string | '' | Campo para sum/avg/min/max |
| total | string | 'count' | count, sum, avg, min, max |
| query | Builder | null | Eloquent/Query Builder pré-montado (alternativa a model+filters) |
| filters | array | [] | Filtros inline: [['campo', 'op', 'val'], ...] |
| joins | array | [] | Joins keyed por tabela: ['tabela' => ['fk', 'pk']] ou ['tabela' => ['fk', 'op', 'pk']]. Ex: ['sale_status' => ['sale.sale_status_id', 'sale_status.id']]. A PK precisa de prefixo explícito (tabela.pk). |
| data | array | null | Dados manuais: ['Label' => valor, ...] (dispensa model) |
| title | string | '' | Título do card |
| subtitle | string | '' | Subtítulo |
| height | int|'auto' | 300 | Altura em px |
| width | string | '100%' | Largura |
| no-panel | bool | false | Sem card wrapper (bare chart) |
| legend | bool | false | Mostrar legenda |
| legend-position | string | 'bottom' | bottom, right |
| format | string | '' | 'currency:R$', 'currency:R$:2:,:.', 'numeric:2' |
| suffix | string | '' | Sufixo nos valores (ex: ' kg') |
| legend-format | string | '' | Formato declarativo dos labels: date|date-short|date-long|datetime|time|year|month|month-short|month-year|month-year-short|weekday|weekday-short|quarter|quarter-year|integer|numeric|currency|percent|abbreviate |
| transformer | callable | null | Transformer de valor. Qualquer callable (closure ou Transformers::nome(...)). Roda 1× por ponto — evite query no banco por valor (N+1). |
| legend-transformer | callable | null | Transformer de categoria/legenda (id → label) |
| sub-legend-transformer | callable | null | Transformer da sub-série (multi-série) |
| options | array | [] | Extra ECharts options (merge bruto) |
| colors | array | [] | Paleta custom |
| horizontal | bool | false | Barras horizontais |
| stacked | bool | false | Barras empilhadas |
| area | bool | false | Line com área preenchida |
| smooth | bool | true | Curva suave (line/area) |
| percentage | bool | false | Mostra % (pie/donut/rose) |
| abbreviate | bool | false | Valores abreviados (K, M, B) |
| filter-prop | string | '' | Prop do host setada ao clicar numa categoria/fatia (ver "Clique-para-filtrar") |
| filter-mode | string | auto | direct, lookup, period — auto-detectado a partir de filter-prop/filter-model/legend-format |
| filter-model | string | '' | Model usado pra resolver o label clicado num ID (modo lookup) |
| filter-field | string | '' | Coluna do filter-model comparada com o label (modo lookup) |
| filter-toggle | bool | true | 2º clique na mesma categoria limpa o filtro |
| click-action | string | '' | Método PHP custom chamado com ($label, $value, $dataIndex) — alternativa 100% manual ao filter-* |
| line-series | array|string | [] |
mixed modo A — séries do group-by que viram linha. Nomes (case-insensitive) ou índices int; CSV aceito. Exige group-by de 2 dimensões |
| line-total | string | '' | mixed modo B — agregação da linha: count, sum, avg, min, max. Exige group-by de 1 dimensão |
| line-field | string | '' | mixed modo B — campo da 2ª agregação. Vazio só é válido com line-total="count" (vira count(*)) |
| line-label | string | '' | mixed modo B — nome da série de linha. Default "{total}({field})" |
| line-secondary-axis | bool | false | mixed A e B — põe a linha no eixo Y da direita |
| class | string | '' | Classes CSS extras |
Uso básico — bar chart com ORM
<mad-db-chart type="bar" model="PedidoVenda" database="minierp"
group-by="status" total="count"
title="Pedidos por Status" height="300" />
Soma de campo
<mad-db-chart type="bar" model="PedidoVenda"
group-by="mes" field="valor_total" total="sum"
title="Vendas por Mês" format="currency:R$" height="350" />
Com query externa (Eloquent Builder pronto)
@php
$query = \App\Models\PedidoVenda::where('ano', '=', date('Y'));
@endphp
<mad-db-chart type="line" :query="$query"
group-by="mes" field="valor_total" total="sum"
title="Vendas do Ano" format="currency:R$" />
Util para reaproveitar a mesma query (período + filtros) de um dashboard via
$that->baseQuery('PedidoVenda') (ver MadDashboard em dashboard.md).
Com filtros inline
<mad-db-chart type="bar" model="PedidoVenda"
:filters="[['ativo', '=', '1']]"
group-by="categoria" total="count"
title="Ativos por Categoria" />
Pie / Donut
<mad-db-chart type="donut" model="Cliente"
group-by="estado" total="count"
title="Clientes por Estado" legend percentage />
Dados manuais (sem model)
<mad-db-chart type="pie" :data="['Eletrônicos' => 4500, 'Roupas' => 2100, 'Alimentos' => 3200]"
title="Distribuição" legend />
Sem card wrapper (bare)
<mad-db-chart type="bar" model="MadQueueJob" database="permission"
group-by="queue" total="count"
title="Jobs por Fila" height="260" no-panel />
Barras horizontais / empilhadas
<mad-db-chart type="bar" model="Vendas"
group-by="categoria" field="valor" total="sum"
title="Por Categoria" horizontal stacked />
Line com área
<mad-db-chart type="line" model="Acesso"
group-by="dia" total="count"
title="Acessos Diários" area height="280" />
Radar (type="radar")
Categorias viram eixos radiais (indicators); cada série vira um polígono. Bom pra comparar perfis multi-dimensão (KPIs por filial, skills por pessoa).
{{-- 1 dimensão → 1 polígono, categorias = eixos --}}
<mad-db-chart type="radar" model="IamUserGroup" database="iam"
group-by="mad_iam_user_group.group_id" total="count"
title="Usuários por grupo" height="320" />
{{-- 2 dimensões → N polígonos (1 por valor da 2ª dim), com legenda --}}
<mad-db-chart type="radar" :query="$vendasQuery"
:group-by="['mes', 'canal']" total="sum" field="valor" legend
title="Vendas por mês × canal" height="360" />
Alias de tag: <mad-radar-chart ... /> ≡ <mad-db-chart type="radar" ... />.
Regras automáticas da engine:
- Max único global em todos os indicators, com folga de 5% arredondada (30 → 40, 730 → 800) — mantém os polígonos comparáveis. Tudo zero → max = 1.
- Negativos são clampados em 0 (o radar do ECharts não tem eixo negativo);
o tooltip ainda mostra o valor raw quando há
transformer. - Combinação categoria×série ausente vira
0(vetores sempre alinhados aos indicators). - Ordem das séries é alfabética (
ksort) —:colorsposicionais funcionam. legend-transformer/legend-formatformatam os NOMES dos indicators;sub-legend-transformerformata os nomes das séries (2ª dimensão).
Limitações do radar
- Sem drill-through:
filter-prop/filter-modesão ignorados (o clique no radar devolve o polígono inteiro, não uma categoria).click-actioncontinua funcionando, com essa semântica. horizontal,stacked,areaesmoothnão se aplicam.- Recomendado ≥ 3 categorias — com 1 ou 2 o polígono degenera em ponto/linha.
legendsó tem efeito comgroup-byde 2 dimensões.
Mixed — colunas + linha (type="mixed")
Dois modos, mutuamente exclusivos (configurar os dois lança exception):
| Modo | Quando usar | group-by | Props |
|---|---|---|---|
| A — séries do group-by | As séries JÁ existem nos dados (2ª dimensão) e algumas devem virar linha | 2 dimensões | line-series |
| B — 2ª métrica | Colunas = uma agregação, linha = OUTRA agregação dos mesmos registros | 1 dimensão | line-total, line-field, line-label |
Modo A — séries do group-by viram linha
{{-- 'Meta' (valor da 2ª dimensão "canal") vira linha sobre as colunas --}}
<mad-db-chart type="mixed" :query="$vendasQuery"
:group-by="['mes', 'canal']" total="sum" field="valor"
line-series="Meta" legend stacked
title="Vendas × Meta" height="340" />
{{-- múltiplas séries + eixo Y secundário --}}
<mad-db-chart type="mixed" :query="$vendasQuery"
:group-by="['mes', 'canal']" total="sum" field="valor"
:line-series="['Meta', 'Média']" line-secondary-axis legend />
O match é por nome case-insensitive contra o nome final da série (depois do
sub-legend-transformer) OU por índice int (posição na ordem alfabética).
Nome sem match é no-op silencioso — renderiza como bar puro.
Modo B — 2ª métrica agregada como linha
A 2ª agregação roda no mesmo SELECT (uma query só, alias line_total):
{{-- colunas = logins/dia; linha = logouts (count de não-nulos) --}}
<mad-db-chart type="mixed" :query="$accessQuery"
group-by="date(mad_log_access.login_time)" total="count"
line-total="count" line-field="mad_log_access.logout_time"
line-label="Logouts" legend
title="Acessos" height="320" />
{{-- colunas = soma; linha = média (eixo secundário: escalas diferem) --}}
<mad-db-chart type="mixed" model="Venda"
group-by="mes" total="sum" field="valor"
line-total="avg" line-field="ticket" line-label="Ticket médio"
line-secondary-axis legend />
Regras do modo B:
- Exige
group-byde 1 dimensão — com 2 dims lança exception ("line-metric exige group-by de 1 dimensão; use line-series"). line-total∈count|sum|avg|min|max(mesmo allowlist dototal).line-fieldvazio só é válido comline-total="count"(viracount(*)).count(campo)conta não-nulos — útil pra "quantos têm X preenchido".line-fieldpassa pelo mesmo guard anti-SQL-injection dofield(OrderGuard::validateExpression).- A série de colunas (single-series não tem nome) ganha
name= título do chart; sobrescreva com:options="['series' => [0 => ['name' => 'Logins']]]". - A legenda vai pro topo (o default bottom do ECharts v6 colide com os labels do eixo X).
Comum aos dois modos
- A linha é
smooth, símbolo circle,z:3(na frente das colunas), cor = próximo stroke da paleta (ou posicional do:colors). line-secondary-axisfaz oyAxisvirar uma lista[esquerdo, direito]e a linha usaryAxisIndex:1— quem sobrescreveyAxisvia:optionsprecisa tratar como lista nesse caso.stackedempilha só as colunas; a linha fica fora do stack (caso clássico: colunas empilhadas + linha de meta).horizontalé ignorado por construção — mixed é sempre vertical.- Click-to-filter funciona normal (
filter-prop/filter-mode): clique na coluna OU no ponto da linha resolve a categoria viadataIndex.
Auto-promoção bar → mixed
type="bar" (ou omitido) + qualquer line-series/line-total promove
automaticamente para mixed. Prefira emitir type="mixed" explícito.
Alias de tag: <mad-mixed-chart ... /> ≡ <mad-db-chart type="mixed" ... />.
Clique-para-filtrar (drill-down)
Em telas de dashboard (MadDashboard), o gráfico pode setar um filtro do
componente host ao clicar numa categoria/fatia — sem JS manual:
{{-- direct: seta a prop "estado_nome" do host com o label clicado --}}
<mad-db-chart type="bar" model="Cliente"
group-by="estado" total="count" title="Por estado"
filter-prop="estado_nome" filter-mode="direct" />
{{-- lookup: resolve o label num registro e seta o ID (FK) --}}
<mad-db-chart type="donut" model="Negociacao"
group-by="estado.nome" total="count" title="Por estado da negociação"
filter-prop="estado_id" filter-mode="lookup"
filter-model="EstadoNegociacao" filter-field="nome" />
{{-- period: categoria é uma coluna ano-mes ("2026-06") — seta mes/ano do dashboard --}}
<mad-db-chart type="line" model="Acesso"
group-by="ano_mes" total="count" title="Acessos por período"
filter-mode="period" legend-format="month-year" />
filter-mode é auto-detectado quando omitido: filter-model setado → lookup;
legend-format de data → period; caso contrário → direct. filter-toggle
(default true) faz o 2º clique na mesma categoria limpar o filtro.
Para handler 100% customizado, use click-action="metodoPhp" — chama
$this->metodoPhp($label, $value, $dataIndex) no host.
SEMPRE usar <mad-db-chart> para gráficos
{{-- 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>
{{-- ERRADO: usar MadChart builder no controller e passar pro Blade --}}
$chart = MadChart::bar('vendas')->fromModel('PedidoVenda')...;
return ['view', ['chart' => $chart]];
<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$" />
Transformer de legenda (id → label)
Para traduzir uma categoria (ex: status_id) num label legível, use a prop
legend-transformer. Aceita qualquer callable — closure ou first-class
callable. O motor chama call_user_func($fn, $valor, $item, $data) 1× por
valor distinto de legenda, então a regra prática NÃO é "closure é proibida" —
é não fazer query no banco por valor (isso vira N+1). Feche sobre um mapa
pré-carregado (1 query), ou use um Transformers::nome(...) que já faz batch:
@php $statusMap = StatusPedido::pluck('nome', 'id')->all(); @endphp
<mad-db-chart type="donut" model="PedidoVenda"
group-by="status_id" total="count" title="Por Status" legend
:legend-transformer="fn($id) => $statusMap[$id] ?? $id" />
{{-- ou, com Transformers global que faz batch internamente: --}}
<mad-db-chart type="donut" model="PedidoVenda"
group-by="status_id" total="count" title="Por Status" legend
:legend-transformer="Transformers::statusPedidoNome(...)" />
Anti-padrão (N+1):
:legend-transformer="fn($v) => StatusPedido::find($v)?->nome"— umfind()por fatia. Tecnicamente funciona, mas dispara uma query por valor.
Quando usar <mad-chart :config="$builder"> (exceção)
Apenas quando precisar de lógica condicional complexa que não pode ser
expressa via props do <mad-db-chart> — ex: múltiplas séries com regras de
negócio distintas. O transformer pode ser uma closure ou Transformers::nome(...)
— só evite lookup no banco por valor (N+1):
@php
$chart = \Mad\Chart\MadChart::bar('custom')
->fromModel('PedidoVenda')
->groupBy('status_id')
->count()
->legendTransformer(Transformers::statusPedidoNome(...));
@endphp
<mad-chart :config="$chart" />