fahadyousuff/pbix-dashboard-skill

Modifier ou ajouter des visuels, des pages et des propriétés visuelles dans un fichier .pbix de Power BI Desktop (format PBIR) sans le corrompre.

Qu'est-ce que pbix-dashboard-skill ?

Cette compétence permet de modifier de manière programmatique les fichiers .pbix de Power BI Desktop utilisant le format PBIR (Enhanced Report Format). Elle est conçue pour les développeurs qui souhaitent automatiser la création ou la modification de tableaux de bord Power BI, par exemple pour l'intégration continue (CI), la génération de modèles en masse ou la personnalisation de dispositions basées sur les données. La compétence prend en charge l'ajout de pages, de visuels (graphiques, cartes KPI, tableaux), la modification de la disposition, le rebinding des champs, et la gestion des erreurs de corruption courantes (comme les erreurs MashupValidationError, Invalid SecurityBindings ou les fichiers considérés comme corrompus). Elle fonctionne avec PowerShell et .NET ZipArchive pour éviter les problèmes de corruption liés à Python zipfile, et garantit l'écriture des fichiers JSON sans BOM UTF-8. La compétence explique également comment se connecter à une session Power BI Desktop en direct pour lire le schéma et les données, et comment styler les visuels en utilisant les noms de propriétés corrects dans les fichiers JSON, en évitant les pièges courants comme les mauvais noms de propriétés qui entraînent une perte de style silencieuse.

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

Installed? Explore more Recherche et analyse de données skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Correcteur de fichiers PBIX (Dashboard Patcher)

Modifier un fichier .pbix de Power BI Desktop sur place : ajouter des pages, ajouter des visuels, modifier la disposition, rebinder des champs — puis le reconditionner pour que Power BI Desktop l'ouvre sans erreur de corruption.

Quand l'utiliser

  • L'utilisateur a un fichier .pbix enregistré et souhaite ajouter des visuels ou générer un tableau de bord.
  • L'utilisateur souhaite automatiser les modifications de rapports Power BI (CI, modélisation en masse, disposition basée sur les données).
  • Une tentative de modification précédente a produit MashupValidationError, Invalid SecurityBindings ou un message de fichier corrompu à l'ouverture.

Le problème central (à lire en premier)

Les fichiers .pbix modernes utilisent le format PBIR (Enhanced Report Format) : la disposition du rapport est stockée sous forme de fichiers JSON par visuel dans Report/definition/pages/<pageId>/visuals/<visualId>/visual.json, et non plus dans l'ancien fichier monolithique Report/Layout.

Un fichier PBIX est une archive ZIP contenant (entre autres) :

  • DataModel — le modèle Analysis Services (ne doit PAS être recompressé ; à conserver tel quel).
  • Report/definition/... — l'arborescence JSON PBIR par visuel.
  • SecurityBindings — un manifeste chiffré via DPAPI qui signe la disposition du fichier.
  • [Content_Types].xml — liste chaque fichier présent dans l'archive ZIP.

Pièges qui corrompent le fichier (tirés de l'expérience) :

  1. zipfile de Python définit des attributs de fichier externes différents de ceux de ZipArchive de .NET. Power BI les valide. Utilisez ZipArchive de .NET via PowerShell, avec ExternalAttributes = 0 sur chaque entrée.
  2. Recompresser DataModel le corrompt. Il doit être stocké avec la même méthode de compression que la source (généralement Deflate mais parfois Stored) ; le plus sûr est NoCompression si vous l'avez réécrit, sinon conservez les octets originaux tels quels.
  3. L'ajout d'un fichier casse SecurityBindings. C'est un manifeste signé de ce qui se trouve dans l'archive ZIP. Si vous ajoutez Report/definition/pages/<new>/visuals/.../visual.json, la signature ne correspond plus et Power BI refuse d'ouvrir le fichier.
  4. Les noms d'entrée ne doivent PAS avoir de / au début. MissingBIPackagePart: /Version à l'ouverture signifie que votre entrée Version a été compressée sous la forme /Version (slash de tête). Substring($src.Length + 1) naïf de PowerShell laisse une barre oblique inverse au début → devient / dans l'archive ZIP → Power BI rejette. Calculez le préfixe à partir du chemin long résolu et appliquez TrimStart('/') à chaque nom d'entrée.
  5. La marque BOM UTF-8 dans Report/definition/**/*.json est fatale. Erreur de Power BI : Only text with UTF8 encoding without BOM (byte order marks) is supported. Detected BOM: 'UTF-8'. Set-Content -Encoding UTF8 et File.WriteAllText sans encodage explicite écrivent TOUS DEUX une marque BOM dans Windows PowerShell 5.1 / .NET Framework. Écrivez toujours le JSON avec New-Object System.Text.UTF8Encoding($false) passé à WriteAllText ou StreamWriter.

La solution pour SecurityBindings : supprimez-le de l'archive ZIP et retirez son entrée de [Content_Types].xml. Power BI Desktop régénère SecurityBindings lors de la prochaine sauvegarde. Le fichier s'ouvre proprement.

Procédure

1. Inspecter le fichier

# Extraire vers un répertoire de travail en utilisant .NET pour que les attributs soient propres
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("Test.pbix", "pbix_src")

Vérifiez s'il s'agit du format PBIR (nouveau) ou hérité :

  • PBIR : contient Report\definition\pages\<pageId>\visuals\<visualId>\visual.json.
  • Hérité : contient uniquement Report\Layout (chaîne JSON binaire). Cette compétence cible le format PBIR.

2. Comprendre le schéma visual.json (version 2.12.0)

Visuel cardVisual minimal (tuile KPI) :

{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
  "name": "<id hexadécimale à 20 caractères>",
  "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
  }
}

Points clés :

  • name est un identifiant hexadécimal de 20 caractères ; doit être unique dans la page.
  • Exemples de visualType : cardVisual, clusteredBarChart, clusteredColumnChart, lineChart, donutChart, pieChart, tableEx, pivotTable.
  • cardVisual utilise le rôle Data. Les graphiques à barres/colonnes/lignes utilisent Category + Y (+ Series optionnel). Les graphiques en anneau/tarte utilisent Category + Y.
  • Aggregation.Function : 0=Sum, 1=Avg, 2=Count, 3=Min, 4=Max, 5=CountNonNull, 6=Median, 7=StDev, 8=Variance.
  • drillFilterOtherVisuals: true est obligatoire ; son omission corrompt le fichier.

3. Enregistrer la page

Report/definition/pages/pages.json :

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

Chaque dossier de page contient un fichier page.json :

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

4. Écrire les fichiers JSON — UTF-8 SANS BOM

Chaque fois que vous modifiez visual.json, page.json, report.json ou tout fichier sous Report/definition/, écrivez-le sans BOM. Windows PowerShell 5.1 ajoute une BOM par défaut.

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

Pour supprimer une BOM déjà écrite par erreur :

$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. Reconditionner — la SEULE méthode fiable

N'utilisez PAS zipfile de Python. Utilisez PowerShell avec ZipArchive de .NET. Détails clés :

  • Les noms d'entrée utilisent des barres obliques et n'ont PAS de / au début.
  • Calculez la longueur du préfixe source à partir de (Get-Item $src).FullName.TrimEnd('\').Length + 1 (même casse/forme de chemin long que FullName des éléments Get-ChildItem). Appliquez TrimStart('/') à chaque nom d'entrée par sécurité.
  • Chaque entrée de texte (y compris [Content_Types].xml) est écrite avec un StreamWriter UTF-8 sans 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('/')

    # SUPPRIMER SecurityBindings — PBI le régénère lors de la sauvegarde
    if ($rel -eq 'SecurityBindings') { return }

    # Modifier Content_Types.xml pour supprimer la référence à 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 ne doit PAS être recompressé
    $level = if ($rel -eq 'DataModel') {
        [System.IO.Compression.CompressionLevel]::NoCompression
    } else {
        [System.IO.Compression.CompressionLevel]::Optimal
    }

    $entry = $zip.CreateEntry($rel, $level)
    $entry.ExternalAttributes = 0   # critique — Python définit une valeur non nulle que PBI rejette
    $es = $entry.Open()
    $sfs = [System.IO.File]::OpenRead($full)
    $sfs.CopyTo($es)
    $sfs.Close(); $es.Close()
}
$zip.Dispose(); $fs.Close()

6. Valider avant d'ouvrir

Quatre vérifications, dans l'ordre :

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

# a) Parties requises présentes, SecurityBindings absent
$z.Entries | Where-Object { $_.FullName -match '^(Version|DataModel|Metadata|Settings|SecurityBindings|\[Content_Types\]\.xml)$' } |
    Select-Object FullName, Length

# b) Aucune entrée avec un '/' au début
$z.Entries | Where-Object { $_.FullName.StartsWith('/') } | ForEach-Object { "MAUVAIS : $($_.FullName)" }

# c) Aucune BOM sur les métadonnées textuelles ou les 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 trouvée : $($e.FullName)"
        }
    }
}

# d) Chaque JSON Report/definition s'analyse
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 INVALIDE : $($e.FullName) — $($_.Exception.Message)" }
    }
}
$z.Dispose()

Attendu : DataModel, Version, Metadata, Settings, [Content_Types].xml présents ; SecurityBindings absent ; pas de barre oblique de tête ; pas de BOM ; tous les JSON s'analysent.

Connexion à une session Power BI Desktop en direct (optionnel — pour la découverte des données)

Si Power BI Desktop est déjà en cours d'exécution, il expose un point de terminaison Analysis Services sur localhost :

# Découvrir le port
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 "Port Analysis Services : $port"
}

Connectez-vous ensuite via ADOMD (installez Power BI Desktop ; la DLL est fournie avec) :

# 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  # espace de noms réel, pas le nom du fichier

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

Utilisez ceci uniquement pour lire le schéma et prévisualiser les nombres ; les visuels finaux sont écrits sous forme de fichiers JSON et reconditionnés comme ci-dessus.

Styles des visuels — les noms de propriétés qui fonctionnent réellement

Les libellés du volet de mise en forme dans Power BI Desktop ne correspondent PAS aux noms de propriétés JSON. Si vous les devinez, Power BI supprime silencieusement les propriétés invalides lors de la prochaine sauvegarde — le fichier s'ouvre correctement, mais votre style disparaît. Construisez toujours les modifications réelles dans Power BI une fois, enregistrez, extrayez, et copiez les noms de propriétés à partir du JSON enregistré.

Où se trouve le style dans visual.json

  • visual.visualContainerObjects — le cadre extérieur de la vignette du visuel (arrière-plan, bordure, ombre portée, barre de titre). S'applique à tous les types de visuels.
  • visual.objects — l'intérieur du visuel (zone de traçage, axes, légende, points de données, valeur de la carte, remplissage intérieur de la carte). Les noms de propriétés ici sont spécifiques à chaque type de visuel et doivent correspondre au schéma visualStyles[visualType] du thème.

Cadre du conteneur (fonctionne pour chaque visuel)

"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 (tuile KPI) — pièges

Le grand nombre n'est PAS callout.color et PAS value.color. Power BI supprime silencieusement les deux.

  • Couleur de la police du grand nombre : value.fontColor (pas color).
  • Police / taille / graisse du grand nombre : value.fontFamily, value.fontSize, value.bold.
  • Étiquette de catégorie sous le nombre : label.fontColor, label.fontFamily, label.fontSize.
  • Remplissage teinté derrière le nombre : cardCalloutArea.backgroundFillColor, avec un selector qui nomme les métadonnées de la projection de mesure — par exemple "selector":{"metadata":"Sum(financials.Profit)"}. Lisez cette chaîne à partir 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)"}
  }]
}

Graphiques à barres / colonnes / lignes / anneau — couleurs des données

  • dataPoint.defaultColor — la couleur de repli utilisée lorsqu'aucune couleur n'est attribuée à des catégories spécifiques.
  • dataPoint.fill — parfois utilisé à la place de defaultColor, en fonction du type de visuel.
"objects": {
  "dataPoint": [{"properties":{
    "defaultColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#3B82F6'"}}}}}
  }}]
}

Tableau et matrice

  • grid : propriétés de la grille (bordures, taille des lignes).
  • columnHeaders : police, alignement, couleur de fond de l'en-tête.
  • rowHeaders : police, alignement, couleur de fond de la première colonne.
  • values : police des cellules de valeur.
"objects": {
  "grid": [{"properties":{
    "outlineColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#CBD5E1'"}}}}},
    "horizontalGridLineStyle":{"expr":{"Literal":{"Value":"'Dotted'"}}}
  }}],
  "columnHeaders": [{"properties":{
    "fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#1E293B'"}}}}},
    "backgroundColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#F1F5F9'"}}}}},
    "fontFamily":{"expr":{"Literal":{"Value":"'Segoe UI Semibold'"}}},
    "fontSize":{"expr":{"Literal":{"Value":"12D"}}}
  }}],
  "values": [{"properties":{
    "fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#334155'"}}}}}
  }}]
}

Filtres au niveau du rapport (dans report.json)

{
  "$schema": "...report/2.5.0/schema.json",
  "filters": [{
    "filter": {
      "$schema": "...filterConfiguration/2.0.0/schema.json",
      "expression": {
        "And": {
          "left": {
            "Comparison": {
              "comparisonType": 2,
              "left": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Year"}},
              "right": {"Literal": {"Value": "'2024'"}}
            }
          }
        }
      },
      "filterState": {"version": 2, "members": [{"memberName": "(All)"}]},
      "filterScope": {"is": {"All": {}}}
    }
  }]
}

Types de comparaison : 0=Égal, 1=Différent, 2=Supérieur, 3=Supérieur ou égal, 4=Inférieur, 5=Inférieur ou égal.

Résumé — Script de correction complet

# 1. Extraire
$srcPbix = "Original.pbix"
$outDir = "pbix_wip"
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($srcPbix, $outDir)

# 2. Ajouter ou modifier des pages/visuels (détaillé ci-dessus)
# ... vos modifications JSON ici ...

# 3. Supprimer SecurityBindings et sa référence dans Content_Types.xml
Remove-Item "$outDir\SecurityBindings" -ErrorAction SilentlyContinue
$ctPath = "$outDir\[Content_Types].xml"
$xml = [System.IO.File]::ReadAllText($ctPath) -replace '<Override[^/]*PartName="/SecurityBindings"[^/]*/>',''
[System.IO.File]::WriteAllText($ctPath, $xml, (New-Object System.Text.UTF8Encoding($false)))

# 4. Reconditionner (codes ci-dessus)
# ...

Write-Host "Terminé. Ouvrez Test_patched.pbix dans Power BI Desktop."

Skills associés