CommunityPesquisa e Análise de Dadosgithub.com

Patcher de Dashboard PBIX

Edite programaticamente .pbix do Power BI Desktop: adicione páginas, visuais, altere layout e religue campos sem corromper o arquivo.

O que é Patcher de Dashboard PBIX?

Este skill permite modificar arquivos .pbix do Power BI Desktop de forma programática, utilizando o formato PBIR Enhanced Report Format. Com ele, é possível adicionar ou editar páginas e visuais, alterar propriedades visuais e religar campos, garantindo que o arquivo abra sem erros de corrupção. O skill aborda problemas comuns, como erros de MashupValidationError e SecurityBindings inválidos, fornecendo soluções para corrigi-los. Ele suporta automação via scripts em PowerShell e pode ser integrado a pipelines de CI/CD para geração em massa de relatórios. O processo envolve a manipulação cuidadosa da estrutura ZIP do .pbix, evitando armadilhas como a recompressão do DataModel, a escrita de BOMs UTF-8 em arquivos JSON e a quebra do SecurityBindings. O resultado é um arquivo .pbix funcional que pode ser aberto diretamente no Power BI Desktop.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/fahadyousuff/pbix-dashboard-skill/tree/HEAD

Installed? Explore more Pesquisa e Análise de Dados skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que Patcher de Dashboard PBIX faz?

Edite um arquivo .pbix do Power BI Desktop: adicione páginas, visuais, altere layout, religue campos — e depois empacote-o novamente para que o Power BI Desktop o abra sem erros de corrupção.

Quando usar

  • O usuário tem um .pbix salvo e deseja adicionar visuais / gerar um dashboard.
  • O usuário quer automatizar edições de relatórios do Power BI (CI, templates em lote, layout orientado a dados).
  • Uma tentativa de edição anterior produziu MashupValidationError, Invalid SecurityBindings ou "arquivo corrompido" ao abrir.

O problema central (leia isso primeiro)

Arquivos .pbix modernos usam o Formato de Relatório Aprimorado PBIR: o layout do relatório é armazenado como arquivos JSON por visual em Report/definition/pages/<pageId>/visuals/<visualId>/visual.json, não no antigo Report/Layout monolítico.

Um PBIX é um ZIP contendo (entre outros):

  • DataModel — o modelo Analysis Services (NÃO deve ser recompactado; mantenha como está).
  • Report/definition/... — árvore JSON PBIR por visual.
  • SecurityBindings — um manifesto criptografado por DPAPI que assina o layout do arquivo.
  • [Content_Types].xml — lista todos os arquivos no ZIP.

Armadilhas que corrompem o arquivo (aprenda com as cicatrizes):

  1. O zipfile do Python define atributos de arquivo externos diferentes do ZipArchive do .NET. O Power BI os valida. Use ZipArchive do .NET via PowerShell, com ExternalAttributes = 0 em cada entrada.
  2. Recomprimir o DataModel o quebra. Ele deve ser armazenado com o mesmo método de compressão da origem (geralmente Deflate, mas às vezes Stored); o mais seguro é NoCompression se você o reescreveu, caso contrário, preserve os bytes originais na íntegra.
  3. Adicionar qualquer arquivo quebra o SecurityBindings. É um manifesto assinado do que está no ZIP. Se você adicionar Report/definition/pages/<new>/visuals/.../visual.json, a assinatura não corresponderá mais e o PBI se recusará a abrir.
  4. Os nomes das entradas NÃO devem ter uma / inicial. MissingBIPackagePart: /Version ao abrir significa que sua entrada Version foi zipada como /Version (barra inicial). O Substring($src.Length + 1) ingênuo do PowerShell deixa uma barra invertida inicial → torna-se / no zip → o PBI rejeita. Calcule o prefixo a partir do caminho longo resolvido e use TrimStart('/') em cada nome de entrada.
  5. BOM UTF-8 em Report/definition/**/*.json é fatal. Erro PBI: Only text with UTF8 encoding without BOM (byte order marks) is supported. Detected BOM: 'UTF-8'. Set-Content -Encoding UTF8 e File.WriteAllText sem uma codificação explícita AMBOS escrevem um BOM no Windows PowerShell 5.1 / .NET Framework. Sempre escreva JSON com New-Object System.Text.UTF8Encoding($false) passado para WriteAllText ou StreamWriter.

A correção para SecurityBindings: remova-o do ZIP e remova sua entrada de [Content_Types].xml. O Power BI Desktop regenera o SecurityBindings no próximo Salvamento. O arquivo abre perfeitamente.

Procedimento

1. Inspecione o arquivo

# Extrair para um diretório de trabalho usando .NET para que os atributos voltem limpos
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("Test.pbix", "pbix_src")

Verifique se é PBIR (novo) ou legado:

  • PBIR: tem Report\definition\pages\<pageId>\visuals\<visualId>\visual.json.
  • Legado: tem apenas Report\Layout (string JSON binária). Este skill é direcionado a PBIR.

2. Entenda o esquema visual.json (versão 2.12.0)

cardVisual mínimo (tile KPI):

{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
  "name": "<20-hex-char id>",
  "position": {"x": 20, "y": 20, "z": 0, "height": 160, "width": 440, "tabOrder": 0},
  "visual": {
    "visualType": "cardVisual",
    "query": {
      "queryState": {
        "Data": {
          "projections": [{
            "field": {
              "Aggregation": {
                "Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}},
                "Function": 0
              }
            },
            "queryRef": "Sum(financials.Sales)",
            "nativeQueryRef": "Sum of Sales",
            "active": true
          }]
        }
      },
      "sortDefinition": {
        "sort": [{"field": {"Aggregation": {"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}}, "Function": 0}}, "direction": "Descending"}],
        "isDefaultSort": true
      }
    },
    "drillFilterOtherVisuals": true
  }
}

Fatos principais:

  • name é um id hexadecimal de 20 caracteres; deve ser único dentro da página.
  • visualType exemplos: cardVisual, clusteredBarChart, clusteredColumnChart, lineChart, donutChart, pieChart, tableEx, pivotTable.
  • cardVisual usa a função Data. Barra/coluna/linha usam Category + Y (+ opcional Series). Donut/pizza usam Category + Y.
  • Aggregation.Function: 0=Soma, 1=Média, 2=Contagem, 3=Mín, 4=Máx, 5=ContagemNonNull, 6=Mediana, 7=StDev, 8=Variância.
  • drillFilterOtherVisuals: true é obrigatório; omiti-lo corrompe o arquivo.

3. Registre a página

Report/definition/pages/pages.json:

{"$schema":"...pagesMetadata/1.1.0/schema.json","pageOrder":["<pageId1>","<pageId2>"],"activePageName":"<pageIdX>"}

Cada pasta de página tem um page.json:

{"$schema":"...page/2.1.0/schema.json","name":"<pageId>","displayName":"Dashboard","displayOption":"FitToPage","height":1080,"width":1920}

4. Escreva arquivos JSON — UTF-8 SEM BOM

Toda vez que você editar visual.json, page.json, report.json ou qualquer arquivo em Report/definition/, escreva-o sem BOM. O Windows PowerShell 5.1 adiciona um BOM por padrão.

$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $json, $utf8NoBom)

Para remover um BOM já escrito por um erro anterior:

$b = [System.IO.File]::ReadAllBytes($path)
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
    $text = [System.Text.Encoding]::UTF8.GetString($b, 3, $b.Length - 3)
    [System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding($false)))
}

5. Reempacotamento — o ÚNICO método confiável

NÃO use zipfile do Python. Use PowerShell com ZipArchive do .NET. Detalhes importantes:

  • Nomes de entrada usam barras normais e NÃO têm / inicial.
  • Calcule o comprimento do prefixo fonte a partir de (Get-Item $src).FullName.TrimEnd('\').Length + 1 (mesmo caso/formato de caminho longo que FullName em itens do Get-ChildItem). Use TrimStart('/') em cada nome de entrada como garantia.
  • Cada entrada de texto (incluindo [Content_Types].xml) é escrita com um StreamWriter UTF-8 sem BOM.
Add-Type -AssemblyName System.IO.Compression

$src = "pbix_src"
$dst = "Test_patched.pbix"
if (Test-Path $dst) { Remove-Item $dst }

$srcFull = (Get-Item $src).FullName.TrimEnd('\')
$prefixLen = $srcFull.Length + 1
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)

$fs = [System.IO.File]::Open($dst, 'Create')
$zip = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)

Get-ChildItem $src -Recurse -File | ForEach-Object {
    $full = $_.FullName
    $rel = $full.Substring($prefixLen).Replace('\','/').TrimStart('/')

    # REMOVER SecurityBindings — PBI regera ao salvar
    if ($rel -eq 'SecurityBindings') { return }

    # Editar Content_Types.xml para remover a referência SecurityBindings
    if ($rel -eq '[Content_Types].xml') {
        $xml = [System.IO.File]::ReadAllText($full) -replace '<Override[^/]*PartName="/SecurityBindings"[^/]*/>',''
        $entry = $zip.CreateEntry($rel, [System.IO.Compression.CompressionLevel]::Optimal)
        $entry.ExternalAttributes = 0
        $sw = New-Object System.IO.StreamWriter($entry.Open(), $utf8NoBom)
        $sw.Write($xml); $sw.Close()
        return
    }

    # DataModel NÃO deve ser recompactado
    $level = if ($rel -eq 'DataModel') {
        [System.IO.Compression.CompressionLevel]::NoCompression
    } else {
        [System.IO.Compression.CompressionLevel]::Optimal
    }

    $entry = $zip.CreateEntry($rel, $level)
    $entry.ExternalAttributes = 0   # crítico — Python define isso como um valor não zero que o PBI rejeita
    $es = $entry.Open()
    $sfs = [System.IO.File]::OpenRead($full)
    $sfs.CopyTo($es)
    $sfs.Close(); $es.Close()
}
$zip.Dispose(); $fs.Close()

6. Valide antes de abrir

Quatro verificações, em ordem:

Add-Type -AssemblyName System.IO.Compression.FileSystem
$z = [System.IO.Compression.ZipFile]::OpenRead("Test_patched.pbix")

# a) Partes necessárias presentes, SecurityBindings ausente
$z.Entries | Where-Object { $_.FullName -match '^(Version|DataModel|Metadata|Settings|SecurityBindings|\[Content_Types\]\.xml)$' } |
    Select-Object FullName, Length

# b) Nenhuma entrada tem '/' inicial
$z.Entries | Where-Object { $_.FullName.StartsWith('/') } | ForEach-Object { "RUIM: $($_.FullName)" }

# c) Nenhum BOM em qualquer metadado de texto ou JSON Report/definition
foreach ($e in $z.Entries) {
    if ($e.FullName -match '\.(json|xml)$|^Version$|^Metadata$|^Settings$') {
        $s = $e.Open(); $ms = New-Object System.IO.MemoryStream; $s.CopyTo($ms); $s.Close()
        $b = $ms.ToArray()
        if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
            Write-Host "BOM encontrado: $($e.FullName)"
        }
    }
}

# d) Todo JSON Report/definition é analisável
foreach ($e in $z.Entries) {
    if ($e.FullName -like 'Report/definition/*.json' -or $e.FullName -like 'Report/definition/**/*.json') {
        $s = $e.Open(); $sr = New-Object System.IO.StreamReader($s)
        $txt = $sr.ReadToEnd(); $sr.Close()
        try { $null = $txt | ConvertFrom-Json } catch { "JSON INVÁLIDO: $($e.FullName) — $($_.Exception.Message)" }
    }
}
$z.Dispose()

Espere: DataModel, Version, Metadata, Settings, [Content_Types].xml presentes; SecurityBindings ausente; sem barras iniciais; sem BOMs; todos JSONs analisáveis.

Conectando a uma sessão ativa do Power BI Desktop (opcional — para descoberta de dados)

Se o PBI Desktop já estiver em execução, ele expõe um endpoint Analysis Services no localhost:

# Descubra a porta
Get-Process msmdsrv | ForEach-Object {
    $port = Get-NetTCPConnection -OwningProcess $_.Id -State Listen |
            Where-Object { $_.LocalAddress -eq '127.0.0.1' } |
            Select-Object -First 1 -ExpandProperty LocalPort
    Write-Host "Porta Analysis Services: $port"
}

Depois conecte via ADOMD (instale o Power BI Desktop; a DLL acompanha):

# pip install pyadomd pythonnet
import clr
clr.AddReference(r"C:\Program Files\Microsoft Power BI Desktop\bin\Microsoft.PowerBI.AdomdClient.dll")
from Microsoft.AnalysisServices.AdomdClient import AdomdConnection  # namespace real, não o nome do arquivo

conn = AdomdConnection(f"Data Source=localhost:{port}")
conn.Open()
# Executar DAX: EVALUATE SUMMARIZECOLUMNS(financials[Segment], "Sales", SUM(financials[Sales]))

Use isso apenas para ler esquema e prévia de números; os visuais finais são escritos como arquivos JSON e reempacotados acima.

Estilizando visuais — os nomes de propriedade que realmente funcionam

Os rótulos do painel de formatação no Power BI Desktop NÃO correspondem aos nomes de propriedade JSON. Se você chutar, o PBI silenciosamente remove as propriedades falsas no próximo salvamento — o arquivo abre bem, mas seu estilo desaparece. Sempre construa alterações reais no PBI uma vez, salve, extraia e copie os nomes de propriedade do JSON salvo.

Onde a estilização reside em visual.json

  • visual.visualContainerObjects — o quadro externo do tile visual (fundo, borda, sombra, barra de título). Aplica-se a todo tipo de visual.
  • visual.objects — o interior do visual (área do gráfico, eixos, legenda, pontos de dados, valor do card, preenchimento interno do card). Os nomes de propriedade aqui são por tipo de visual e devem corresponder ao esquema de visualStyles[visualType] do tema.

Quadro do container (funciona para todo visual)

"visualContainerObjects": {
  "background": [{"properties":{
    "color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#EEF2FF'"}}}}}},
    "transparency":{"expr":{"Literal":{"Value":"0D"}}}
  }}],
  "border": [{"properties":{
    "show":{"expr":{"Literal":{"Value":"true"}}},
    "color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E2E8F0'"}}}}}},
    "radius":{"expr":{"Literal":{"Value":"12D"}}}
  }}],
  "dropShadow": [{"properties":{
    "show":{"expr":{"Literal":{"Value":"true"}}},
    "color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#0F172A'"}}}}}},
    "transparency":{"expr":{"Literal":{"Value":"85D"}}},
    "position":{"expr":{"Literal":{"Value":"'Outer'"}},
    "preset":{"expr":{"Literal":{"Value":"'BottomRight'"}}}
  }}]
}

cardVisual (tile KPI) — armadilhas

O número grande NÃO é callout.color e NÃO é value.color. O PBI silenciosamente descarta ambos.

  • Cor da fonte do número grande: value.fontColor (não color).
  • Fonte / tamanho / peso do número grande: value.fontFamily, value.fontSize, value.bold.
  • Rótulo da categoria abaixo do número: label.fontColor, label.fontFamily, label.fontSize.
  • Preenchimento colorido atrás do número: cardCalloutArea.backgroundFillColor, com um selector que nomeia os metadados da projeção da medida — por exemplo, "selector":{"metadata":"Sum(financials.Profit)"}. Leia essa string de visual.query.queryState.Data.projections[0].queryRef.
"objects": {
  "value": [{"properties":{
    "fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#4F46E5'"}}}}}},
    "fontFamily":{"expr":{"Literal":{"Value":"'Segoe UI Semibold'"}},
    "fontSize":{"expr":{"Literal":{"Value":"40D"}}},
    "bold":{"expr":{"Literal":{"Value":"true"}}}
  }}],
  "label": [{"properties":{
    "fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#64748B'"}}}}}}
  }}],
  "cardCalloutArea": [{
    "properties": {
      "show":{"expr":{"Literal":{"Value":"true"}}},
      "backgroundFillColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E0E7FF'"}}}}}}
    },
    "selector":{"metadata":"Sum(financials.Sales)"}
  }]
}

Barra / coluna / linha / donut — cores de dados

  • dataPoint.defaultColor — a cor de fallback

… (truncado para tradução)

Habilidades Relacionadas