Skip to content

Tutorial: Tabela Pivot com dados inline

Neste tutorial você criará o relatório Arrecadação de ICMS por Estado — 2025: registros planos {estado, mes, icms} são transformados automaticamente em uma matriz Estado × Mês com totais por linha e coluna, usando o pivot mode do TableElement.

O que você vai aprender:

  • Estruturar dados flat para consumo pivot
  • Configurar TablePivotConfig com rowFields, columnField, valueField e verb
  • Usar height: 0 em SummaryBand para altura dinâmica (PDF)
  • Ativar totais por linha (showRowTotals) e por coluna (showColumnTotals)
  • Configurar o pivot no Studio via editor de tabela

O código completo deste exemplo está em example/lib/examples/icms_pivot_report.dart.

Pré-requisitos

  1. Sulfite instalado (Instalação →)
  2. Familiaridade com data sources e bands

Passo 1 — Entender o modelo de dados

O pivot mode espera registros planos: um registro por célula da matriz final.

dart
// 8 estados × 12 meses = 96 registros
[
  {'estado': 'SP', 'mes': 'Jan', 'icms': 180.0},
  {'estado': 'SP', 'mes': 'Fev', 'icms': 162.0},
  // ...
  {'estado': 'GO', 'mes': 'Dez', 'icms': 47.0},
]

O renderer agrupa, pivota e agrega esses registros em tempo de renderização — o JSON do relatório não muda conforme os dados crescem.

Declare o data source normalmente:

json
{
  "dataSources": [
    {
      "id": "arrecadacao",
      "type": "list",
      "schema": {
        "estado": "string",
        "mes": "string",
        "icms": "number"
      }
    }
  ]
}

Passo 2 — Configurar o pivot

No elemento table, adicione o campo pivot com TablePivotConfig. Quando pivot está presente, as columns são ignoradas — os cabeçalhos são gerados dinamicamente a partir dos valores distintos de columnField.

json
{
  "type": "table",
  "id": "pivot_icms",
  "x": 0,
  "y": 10,
  "width": 782,
  "height": 0,
  "dataSourceId": "arrecadacao",
  "showHeader": true,
  "headerFill": "2e7d32",
  "headerTextColor": "ffffff",
  "headerBold": true,
  "headerFontSize": 8,
  "headerHeight": 20,
  "rowHeight": 18,
  "columnFontSize": 8,
  "rowTextColor": "1b5e20",
  "evenRowFill": "e8f5e9",
  "oddRowFill": "ffffff",
  "alternateRowColors": true,
  "borderColor": "c8e6c9",
  "borderWidth": 0.5,
  "showRowBorders": true,
  "showColumnBorders": true,
  "columns": [],
  "pivot": {
    "rowFields": ["estado"],
    "columnField": "mes",
    "valueField": "icms",
    "verb": "SUM",
    "showRowTotals": true,
    "showColumnTotals": true,
    "totalLabel": "Total"
  }
}

Campos de pivot

Obrigatórios

CampoDescrição
rowFieldsLista de campos que formam os rótulos de linha. Suporta múltiplos para agrupamento hierárquico (ex: ["regiao", "estado"])
columnFieldCampo cujos valores distintos viram cabeçalhos de coluna (ex: "mes")
valueFieldCampo numérico a agregar em cada célula (ex: "icms")

Agregação

CampoPadrãoDescrição
verb"SUM"Função de agregação: "SUM", "AVG", "COUNT", "MIN", "MAX"

O total de linha e o total de coluna respeitam o verb escolhido. Para AVG, o total é a média real de todos os valores — não a média das médias por célula.

Totais

CampoPadrãoDescrição
showRowTotalstrueAdiciona coluna de total ao final de cada linha
showColumnTotalstrueAdiciona linha de totais no rodapé da tabela
totalLabel"Total"Rótulo da coluna/linha de totais. String vazia usa "Total" em tempo de renderização
totalsFillherda headerFillCor de fundo da linha de totais (hex sem #)
totalsTextColorherda headerTextColorCor do texto da linha de totais
totalsBoldtrueTexto em negrito na linha de totais
totalsFontSizeherda columnFontSizeTamanho de fonte da linha de totais

Ordenação de colunas

CampoPadrãoDescrição
columnOrder[]Lista de valores fixados no início, nessa ordem (ex: ["Jan","Fev",...]). Valores ausentes seguem columnSort
columnSort"alpha"Estratégia para colunas não fixadas: "alpha" A→Z, "alpha_desc" Z→A, "value_asc" menor total primeiro, "value_desc" maior total primeiro, "natural" ordem de aparição nos dados

Estilo avançado

CampoPadrãoDescrição
columnOverrides{}Mapa { "colValue": TableColumnOverride } com overrides de largura, formato, alinhamento, negrito, cor de cabeçalho e conditionalStyles por coluna
cellConditionalStyles[]Regras globais de estilo condicional aplicadas a todas as células de dados. columnOverrides[col].conditionalStyles tem prioridade. Primeira regra satisfeita vence

Passo 3 — Usar SummaryBand com altura dinâmica

O número de linhas pivot varia conforme os dados. Use height: 0 na SummaryBand para que o PDF calcule a altura automaticamente:

json
{
  "type": "summary",
  "id": "summary_pivot",
  "height": 0,
  "elements": [
    { "type": "table", "id": "pivot_icms", ... }
  ]
}

height: 0 em um band com um único TableElement ativa o modo de altura dinâmica no PDF renderer. Os outros formatos (HTML, CSV, Excel) ignoram essa configuração.


Passo 4 — Dimensionar a tabela

Com rowFields: ["estado"] (1 campo), 12 meses e 1 coluna de total, a tabela tem 14 colunas. O renderer distribui width igualmente entre elas:

largura por coluna = width / total_colunas = 782 / 14 ≈ 55.9 pt

Para A4 landscape com margens de 30pt:

largura disponível = 842 − 30 − 30 = 782 pt

Ajuste width conforme a orientação e as margens do seu relatório. Com muitas colunas, prefira A4 landscape ou reduza columnFontSize e headerFontSize para 7–8pt.


Passo 5 — Estrutura completa do relatório

json
{
  "reportId": "icms_pivot_001",
  "reportName": "Arrecadação de ICMS por Estado — 2025",
  "pageSettings": {
    "format": "A4",
    "orientation": "landscape",
    "margins": { "left": 30, "right": 30, "top": 30, "bottom": 30 }
  },
  "dataSources": [
    {
      "id": "arrecadacao",
      "type": "list",
      "schema": { "estado": "string", "mes": "string", "icms": "number" }
    }
  ],
  "bands": [
    {
      "type": "header",
      "id": "header_main",
      "height": 72,
      "elements": [
        {
          "type": "rect",
          "id": "header_bg",
          "x": 0, "y": 0, "width": 782, "height": 72,
          "fill": "1b5e20", "hasBorder": false
        },
        {
          "type": "text",
          "id": "title",
          "x": 20, "y": 10, "width": 600, "height": 26,
          "content": "ARRECADAÇÃO DE ICMS POR ESTADO",
          "fontSize": 20, "bold": true, "color": "ffffff"
        },
        {
          "type": "text",
          "id": "subtitle",
          "x": 20, "y": 38, "width": 580, "height": 14,
          "content": "Demonstrativo mensal — Exercício 2025  ·  Valores em R$ milhões",
          "fontSize": 10, "color": "a5d6a7"
        }
      ]
    },
    {
      "type": "summary",
      "id": "summary_pivot",
      "height": 0,
      "elements": [
        {
          "type": "table",
          "id": "pivot_icms",
          "x": 0, "y": 10, "width": 782, "height": 0,
          "dataSourceId": "arrecadacao",
          "showHeader": true,
          "headerFill": "2e7d32",
          "headerTextColor": "ffffff",
          "headerBold": true,
          "headerFontSize": 8,
          "headerHeight": 20,
          "rowHeight": 18,
          "columnFontSize": 8,
          "rowTextColor": "1b5e20",
          "evenRowFill": "e8f5e9",
          "oddRowFill": "ffffff",
          "alternateRowColors": true,
          "borderColor": "c8e6c9",
          "borderWidth": 0.5,
          "showRowBorders": true,
          "showColumnBorders": true,
          "columns": [],
          "pivot": {
            "rowFields": ["estado"],
            "columnField": "mes",
            "valueField": "icms",
            "verb": "SUM",
            "showRowTotals": true,
            "showColumnTotals": true,
            "totalLabel": "Total"
          }
        }
      ]
    },
    {
      "type": "footer",
      "id": "footer_main",
      "height": 24,
      "elements": [
        {
          "type": "text",
          "id": "footer_page",
          "x": 580, "y": 6, "width": 202, "height": 11,
          "content": "Página {pageNumber} de {totalPages}",
          "fontSize": 7, "italic": true, "color": "bdbdbd", "align": "right"
        }
      ]
    }
  ]
}

Passo 6 — Executar no exemplo

O exemplo já está registrado na demo app. Para rodá-lo:

bash
cd sulfite/example
fvm flutter run

Selecione "📊 ICMS por Estado (Pivot)" na lista de demos. O relatório gera em PDF e HTML com a mesma matrix pivotada.


Variações comuns

Pivot hierárquico (dois rowFields)

Agrupamento por região e estado — cada combinação vira uma linha:

json
"pivot": {
  "rowFields": ["regiao", "estado"],
  "columnField": "mes",
  "valueField": "icms",
  "verb": "SUM",
  "showRowTotals": true,
  "showColumnTotals": true,
  "totalLabel": "Total"
}

Contagem em vez de soma

Para contar quantas transações ocorreram por estado/mês:

json
"pivot": {
  "rowFields": ["estado"],
  "columnField": "mes",
  "valueField": "quantidade",
  "verb": "COUNT",
  "showRowTotals": true,
  "showColumnTotals": false
}

Sem totais, label personalizado

json
"pivot": {
  "rowFields": ["categoria"],
  "columnField": "trimestre",
  "valueField": "receita",
  "verb": "AVG",
  "showRowTotals": false,
  "showColumnTotals": false,
  "totalLabel": "Média Geral"
}

Ordenar colunas por valor (maior total primeiro)

Use columnSort: "value_desc" para listar os meses com maior arrecadação à esquerda:

json
"pivot": {
  "rowFields": ["estado"],
  "columnField": "mes",
  "valueField": "icms",
  "verb": "SUM",
  "columnSort": "value_desc",
  "showRowTotals": true,
  "showColumnTotals": true
}
columnSortComportamento
"alpha"Alfabético A→Z (padrão)
"alpha_desc"Alfabético Z→A
"value_asc"Crescente pelo total agregado da coluna
"value_desc"Decrescente pelo total agregado da coluna (maior primeiro)
"natural"Ordem de aparição nos dados

Dica: Para meses em português na ordem correta, use columnOrder para fixar a sequência e columnSort: "natural" para os demais:

json
"pivot": {
  "rowFields": ["estado"],
  "columnField": "mes",
  "valueField": "icms",
  "verb": "SUM",
  "columnOrder": ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun",
                  "Jul", "Ago", "Set", "Out", "Nov", "Dez"],
  "columnSort": "natural"
}

Overrides de estilo por coluna

Use columnOverrides para configurar largura, formato, alinhamento e cor de cabeçalho de colunas específicas:

json
"pivot": {
  "rowFields": ["estado"],
  "columnField": "mes",
  "valueField": "icms",
  "verb": "SUM",
  "showRowTotals": true,
  "columnOverrides": {
    "Total": {
      "bold": true,
      "headerColor": "1a237e",
      "align": "right",
      "format": "#,##0.00"
    },
    "Dez": {
      "headerColor": "b71c1c"
    }
  }
}

Estilos condicionais nas células

Use cellConditionalStyles para colorir células com base nos dados. As regras são avaliadas em ordem e a primeira que satisfaz a condição é aplicada:

json
"pivot": {
  "rowFields": ["estado"],
  "columnField": "mes",
  "valueField": "icms",
  "verb": "SUM",
  "cellConditionalStyles": [
    { "when": "value > 200", "fill": "e8f5e9", "bold": true },
    { "when": "value < 50",  "textColor": "e53935" }
  ]
}

Para aplicar estilos a uma coluna específica (com prioridade sobre cellConditionalStyles), use columnOverrides[col].conditionalStyles:

json
"columnOverrides": {
  "Total": {
    "conditionalStyles": [
      { "when": "value > 1000", "fill": "1b5e20", "textColor": "ffffff", "bold": true }
    ]
  }
}

O contexto de avaliação de cada regra contém todos os campos da linha mais value (valor bruto antes da formatação).


Configurar via Studio

No editor de tabela (clique duplo na tabela ou ícone de lápis), role até o card Modo Pivot:

  1. Ative o toggle Ativar Pivot
  2. Preencha Campos de Linha — nomes dos campos separados por vírgula (ex: estado)
  3. Preencha Campo de Coluna (ex: mes)
  4. Preencha Campo de Valor (ex: icms)
  5. Escolha a Agregação no dropdown (SUM / COUNT / AVG / MIN / MAX)
  6. Ative/desative Totais por Linha e Totais por Coluna
  7. Ajuste o Rótulo do Total se necessário
  8. Salve — o preview atualiza imediatamente

Desativar o toggle preserva a configuração anterior. Reativar restaura os campos já preenchidos.


Limitações conhecidas

LimitaçãoImpacto
CSV/ExcelExporta os dados planos não pivotados — o pivot ocorre apenas no PDF e HTML
Múltiplos valueFieldsUm único valueField por configuração pivot. Para exibir várias métricas, use tabelas separadas

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro