fahadyousuff/pbix-dashboard-skill

Agrega o edita programáticamente objetos visuales, páginas y propiedades visuales dentro de un archivo .pbix de Power BI Desktop (Formato de Informe Mejorado PBIR) sin dañarlo.

¿Qué es pbix-dashboard-skill?

Esta habilidad está diseñada para desarrolladores y analistas que necesitan modificar, automatizar o generar paneles de Power BI directamente en un archivo .pbix guardado, utilizando el Formato de Informe Mejorado (PBIR). Permite agregar páginas, objetos visuales como tarjetas o gráficos, cambiar el diseño y reenlazar campos, todo mediante la manipulación directa de archivos JSON dentro del ZIP del .pbix. Es especialmente útil para procesos de CI/CD, plantillas masivas o para reparar archivos .pbix que se abren como 'corruptos' debido a errores de manipulación manual. La habilidad se enfoca en plataformas de IA como Claude Code, Cursor, o cualquier agente que pueda ejecutar scripts de PowerShell o Python. Explica detalladamente los problemas comunes que corrompen el archivo, como el uso incorrecto de compresión en el 'DataModel', la presencia de BOM UTF-8 en los archivos JSON, o la necesidad de eliminar el 'SecurityBindings' al agregar nuevos elementos. Proporciona un procedimiento completo con código PowerShell para extraer, editar y reempaquetar el .pbix de forma segura, incluyendo scripts de validación para asegurar la integridad del archivo antes de abrirlo en Power BI Desktop.

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

Installed? Explore more Investigación y análisis de datos skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

PBIX Dashboard Patcher (Parcheador de PBIX para Paneles)

Edita un archivo .pbix de Power BI Desktop en su lugar: añade páginas, añade objetos visuales, cambia el diseño, reenlaza campos y vuelve a empaquetarlo para que Power BI Desktop lo abra sin errores de corrupción.

Cuándo usarlo

  • El usuario tiene un .pbix guardado y quiere añadir objetos visuales o generar un panel.
  • El usuario quiere automatizar las ediciones de informes de Power BI (CI, plantillas masivas, diseño basado en datos).
  • Un intento de edición anterior produjo MashupValidationError, Invalid SecurityBindings o un "archivo corrupto" al abrir.

El problema central (léelo primero)

Los archivos .pbix modernos utilizan el Formato de Informe Mejorado PBIR: el diseño del informe se almacena como archivos JSON por objeto visual en Report/definition/pages/<pageId>/visuals/<visualId>/visual.json, no en el antiguo monolítico Report/Layout.

Un PBIX es un ZIP que contiene (entre otros):

  • DataModel — el modelo de Analysis Services (NO debe ser re-comprimido; almacenar tal cual).
  • Report/definition/... — árbol JSON PBIR por objeto visual.
  • SecurityBindings — un manifiesto cifrado con DPAPI que firma el diseño del archivo.
  • [Content_Types].xml — lista cada archivo en el ZIP.

Problemas que corrompen el archivo (aprendidos de cicatrices):

  1. Python zipfile establece atributos de archivo externos que difieren de ZipArchive de .NET. Power BI los valida. Usa ZipArchive de .NET a través de PowerShell, con ExternalAttributes = 0 en cada entrada.
  2. Re-comprimir DataModel lo rompe. Debe almacenarse con el mismo método de compresión que el origen (normalmente Deflate pero a veces Stored); lo más seguro es NoCompression si lo reescribiste, de lo contrario conserva los bytes originales textualmente.
  3. Agregar cualquier archivo rompe SecurityBindings. Es un manifiesto firmado de lo que hay en el ZIP. Si añades Report/definition/pages/<nueva>/visuals/.../visual.json, la firma ya no coincide y PBI se niega a abrirlo.
  4. Los nombres de las entradas NO deben tener una / al inicio. MissingBIPackagePart: /Version al abrir significa que tu entrada Version fue comprimida como /Version (barra inicial). El Substring($src.Length + 1) ingenuo de PowerShell deja una barra invertida inicial → se convierte en / en el zip → PBI lo rechaza. Calcula el prefijo desde la ruta larga resuelta y aplica TrimStart('/') a cada nombre de entrada.
  5. La marca BOM UTF-8 en Report/definition/**/*.json es fatal. Error de PBI: Only text with UTF8 encoding without BOM (byte order marks) is supported. Detected BOM: 'UTF-8'. Set-Content -Encoding UTF8 y File.WriteAllText sin una codificación explícita AMBOS escriben una BOM en Windows PowerShell 5.1 / .NET Framework. Siempre escribe JSON con New-Object System.Text.UTF8Encoding($false) pasado a WriteAllText o StreamWriter.

La solución para SecurityBindings: elimínalo del ZIP y elimina su entrada de [Content_Types].xml. Power BI Desktop regenera SecurityBindings en el siguiente guardado. El archivo se abre limpiamente.

Procedimiento

1. Inspeccionar el archivo

# Extraer a un directorio de trabajo usando .NET para que los atributos vuelvan limpios
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("Test.pbix", "pbix_src")

Comprueba si es PBIR (nuevo) o heredado:

  • PBIR: tiene Report\definition\pages\<pageId>\visuals\<visualId>\visual.json.
  • Heredado: solo tiene Report\Layout (cadena JSON binaria). Esta habilidad se enfoca en PBIR.

2. Entender el esquema visual.json (versión 2.12.0)

Objeto visual cardVisual mínimo (mosaico KPI):

{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
  "name": "<id-hexadecimal-de-20-caracteres>",
  "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
  }
}

Datos clave:

  • name es un id hexadecimal de 20 caracteres; debe ser único dentro de la página.
  • Ejemplos de visualType: cardVisual, clusteredBarChart, clusteredColumnChart, lineChart, donutChart, pieChart, tableEx, pivotTable.
  • cardVisual usa el rol Data. Los gráficos de barras/columnas/líneas usan Category + Y (+ opcional Series). Los gráficos de dona/tarta usan Category + Y.
  • Aggregation.Function: 0=Suma, 1=Promedio, 2=Conteo, 3=Mínimo, 4=Máximo, 5=ConteoNoNulo, 6=Mediana, 7=DesvEst, 8=Varianza.
  • drillFilterOtherVisuals: true es obligatorio; omitirlo corrompe el archivo.

3. Registrar la página

Report/definition/pages/pages.json:

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

Cada carpeta de página tiene un page.json:

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

4. Escribir archivos JSON — UTF-8 SIN BOM

Cada vez que edites visual.json, page.json, report.json o cualquier archivo bajo Report/definition/, escríbelo sin BOM. Windows PowerShell 5.1 añade una BOM por defecto.

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

Para eliminar una BOM ya escrita por un error anterior:

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

5. Reempaquetar — el ÚNICO método fiable

NO uses Python zipfile. Usa PowerShell con .NET ZipArchive. Detalles clave:

  • Los nombres de las entradas usan barras inclinadas hacia adelante y NO tienen / inicial.
  • Calcula la longitud del prefijo de origen desde (Get-Item $origen).FullName.TrimEnd('\').Length + 1 (misma mayúsculas/minúsculas y forma de ruta larga que FullName en elementos de Get-ChildItem). Aplica TrimStart('/') a cada nombre de entrada como seguro.
  • Cada entrada de texto (incluyendo [Content_Types].xml) se escribe con un StreamWriter UTF-8 sin BOM.
Add-Type -AssemblyName System.IO.Compression

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

$origenCompleto = (Get-Item $origen).FullName.TrimEnd('\')
$prefijoTam = $origenCompleto.Length + 1
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)

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

Get-ChildItem $origen -Recurse -File | ForEach-Object {
    $completo = $_.FullName
    $relativo = $completo.Substring($prefijoTam).Replace('\','/').TrimStart('/')

    # ELIMINAR SecurityBindings — PBI lo regenera al guardar
    if ($relativo -eq 'SecurityBindings') { return }

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

    # DataModel NO debe ser re-comprimido
    $nivel = if ($relativo -eq 'DataModel') {
        [System.IO.Compression.CompressionLevel]::NoCompression
    } else {
        [System.IO.Compression.CompressionLevel]::Optimal
    }

    $entrada = $zip.CreateEntry($relativo, $nivel)
    $entrada.ExternalAttributes = 0   # crítico — Python establece esto a un valor no cero que PBI rechaza
    $es = $entrada.Open()
    $sfs = [System.IO.File]::OpenRead($completo)
    $sfs.CopyTo($es)
    $sfs.Close(); $es.Close()
}
$zip.Dispose(); $fs.Close()

6. Validar antes de abrir

Cuatro comprobaciones, en orden:

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

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

# b) Ninguna entrada tiene una '/' inicial
$z.Entries | Where-Object { $_.FullName.StartsWith('/') } | ForEach-Object { "MAL: $($_.FullName)" }

# c) Sin BOM en ningún metadato de texto o JSON de 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 encontrada: $($e.FullName)"
        }
    }
}

# d) Cada JSON de Report/definition se analiza
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()

Esperado: DataModel, Version, Metadata, Settings, [Content_Types].xml presentes; SecurityBindings ausente; sin barras inclinadas iniciales; sin BOM; todos los JSON se analizan.

Conectarse a una sesión activa de Power BI Desktop (opcional — para descubrimiento de datos)

Si PBI Desktop ya se está ejecutando, expone un punto final de Analysis Services en localhost:

# Descubrir el puerto
Get-Process msmdsrv | ForEach-Object {
    $puerto = Get-NetTCPConnection -OwningProcess $_.Id -State Listen |
            Where-Object { $_.LocalAddress -eq '127.0.0.1' } |
            Select-Object -First 1 -ExpandProperty LocalPort
    Write-Host "Puerto de Analysis Services: $puerto"
}

Luego conéctate a través de ADOMD (instala Power BI Desktop; la DLL se incluye con él):

# 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  # espacio de nombres real, no el nombre del archivo

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

Usa esto solo para leer esquema y previsualizar números; los objetos visuales finales se escriben como archivos JSON y se reempaquetan como se indicó anteriormente.

Dar estilo a los objetos visuales — los nombres de propiedades que realmente funcionan

Las etiquetas del panel de formato en Power BI Desktop NO coinciden con los nombres de propiedades JSON. Si adivinas, PBI elimina silenciosamente las propiedades falsas en el siguiente guardado — el archivo se abre bien, pero tu estilo desaparece. Siempre construye cambios reales en PBI una vez, guarda, extrae y copia los nombres de las propiedades del JSON guardado.

Dónde vive el estilo en visual.json

  • visual.visualContainerObjects — el marco exterior del mosaico visual (fondo, borde, sombra, barra de título). Se aplica a cada tipo de objeto visual.
  • visual.objects — el interior del objeto visual (área de trazado, ejes, leyenda, puntos de datos, valor de tarjeta, relleno interior de tarjeta). Los nombres de propiedades aquí son por tipo de objeto visual y deben coincidir con el esquema visualStyles[visualType] del tema.

Marco del contenedor (funciona para cada objeto 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 (mosaico KPI) — problemas comunes

El número grande NO es callout.color y NO es value.color. PBI elimina silenciosamente ambos.

  • Color de fuente del número grande: value.fontColor (no color).
  • Fuente / tamaño / peso del número grande: value.fontFamily, value.fontSize, value.bold.
  • Etiqueta de categoría debajo del número: label.fontColor, label.fontFamily, label.fontSize.
  • Relleno tintado detrás del número: cardCalloutArea.backgroundFillColor, con un selector que nombra los metadatos de la proyección de la medida — p.ej. "selector":{"metadata":"Sum(financials.Profit)"}. Lee esa cadena 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)"}
  }]
}

Bar / columna / línea / dona — colores de datos

  • dataPoint.defaultColor — el color de reserva

…

[truncado para la traducción]

Skills relacionados