Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now! Learn more

kushanNa

Real Time Cross-Report Filtering Using JavaScript embedded analytics

Introduction

 

When working with multiple Power BI reports, it’s often useful to synchronize slicers across them .

This guide walks you through how to build an HTML+JavaScript web page that does exactly that: two Power BI reports displayed side-by-side, with slicers in Report 1 automatically applying corresponding filters to Report 2 in real-time.

Let’s break it down step by step.

 

🧠 Prerequisites

 

Before you start, make sure you have:

  1. A Power BI Pro or Premium account
  2. Two published reports
  3. Your Tenant ID, Client ID (App ID), Group (Workspace) ID, and Report IDs
  4. A registered Azure App with redirect URI http://localhost:5500
  5. Grant Power BI service Report Read and Dataset read permission to this app 
  6. You can follow this video to learn how to create this Azure App  https://www.youtube.com/watch?v=3xpi7youCNI&list=PL1N57mwBHtN0oPLD8F-Xpp_Y10PqyO-TL&index=8  

 

💻 Full Code

 

We’ll start with the full HTML file (later we’ll explain each section in detail).

👉 Save this as index.html:

 

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Power BI - Dual Slicer Cross Report Filtering</title>
<script src="https://alcdn.msauth.net/browser/2.35.0/js/msal-browser.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/powerbi-client@2.22.3/dist/powerbi.js"></script>
<style>
  body { font-family: Arial, sans-serif; }
  #reportsContainer { display: flex; gap: 20px; margin-top: 20px; }
  .reportBox { flex: 1; height: 540px; border:1px solid #ccc; }
  button { margin-right: 10px; margin-bottom: 10px; }
</style>
</head>
<body>
<h2>Power BI Reports Cross Filtering (Two Slicers)</h2>
<button id="signInButton">Sign In & Load Reports</button>
<button id="listSlicersButton" disabled>List Slicers in Report 1</button>
<button id="listVisualsButton" disabled>List Visual IDs in Report 2</button>

<div id="reportsContainer">
  <div id="reportContainerLeft" class="reportBox"></div>
  <div id="reportContainerRight" class="reportBox"></div>
</div>

<script>
const clientId = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const tenantId = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

const reportId1 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const groupId1  = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

const reportId2 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const groupId2  = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

const msalInstance = new msal.PublicClientApplication({
  auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, redirectUri: window.location.origin }
});

const scopes = ["https://analysis.windows.net/powerbi/api/.default"];
let report1, report2;
const models = window['powerbi-client'].models;

// === Sign In & Embed Both Reports ===
document.getElementById("signInButton").onclick = async () => {
  try {
    const loginResponse = await msalInstance.loginPopup({ scopes });
    const tokenResponse = await msalInstance.acquireTokenSilent({ scopes, account: loginResponse.account });
    const accessToken = tokenResponse.accessToken;

    report1 = powerbi.embed(document.getElementById("reportContainerLeft"), {
      type: "report",
      tokenType: models.TokenType.Aad,
      accessToken,
      id: reportId1,
      embedUrl:`https://app.powerbi.com/reportEmbed?reportId=${reportId1}&groupId=${groupId1}`,
      settings: { panes: { filters: { visible: false } } }
    });

    report2 = powerbi.embed(document.getElementById("reportContainerRight"), {
      type: "report",
      tokenType: models.TokenType.Aad,
      accessToken,
      id: reportId2,
      embedUrl:`https://app.powerbi.com/reportEmbed?reportId=${reportId2}&groupId=${groupId2}`,
      settings: { panes: { filters: { visible: false } } }
    });

    report1.on("loaded", () => {
      console.log("✅ Report 1 loaded");
      document.getElementById("listSlicersButton").disabled = false;
      document.getElementById("listVisualsButton").disabled = false;
      setupDualSlicerListener();
    });

    report2.on("loaded", () => console.log("✅ Report 2 loaded"));
  } catch(err){
    console.error("❌ Authentication failed:", err);
  }
};

// === List slicers in Report 1 ===
document.getElementById("listSlicersButton").onclick = async () => {
  if(!report1) return;
  const pages = await report1.getPages();
  for(let page of pages){
    await page.setActive();
    const visuals = await page.getVisuals();
    for(let visual of visuals){
      if(visual.type === "slicer"){
        console.log(`Slicer title: "${visual.title}", ID: "${visual.name}"`);
      }
    }
  }
};

// === List visuals in Report 2 ===
document.getElementById("listVisualsButton").onclick = async () => {
  if(!report2) return;
  const pages = await report2.getPages();
  const page = pages.find(p => p.isActive) || pages[0];
  await page.setActive();
  const visuals = await page.getVisuals();
  for(let visual of visuals){
    console.log(`Visual title: "${visual.title}", name: "${visual.name}", type: "${visual.type}"`);
  }
};

// === Dual slicer polling listener ===
async function setupDualSlicerListener() {
  const pages1 = await report1.getPages();
  const page1 = pages1.find(p => p.isActive) || pages1[0];
  const visuals1 = await page1.getVisuals();

  // Slicer 1 → Country
  const slicerCountry = visuals1.find(v => v.type === "slicer" && v.name === "aebf94a0ac0055d793ea");
  // Slicer 2 → Age
  const slicerAge = visuals1.find(v => v.type === "slicer" && v.name === "881d066a54a44db7a330");

  if(!slicerCountry || !slicerAge) {
    console.error("❌ One or both slicers not found!");
    return;
  }

  let lastCountryValues = [];
  let lastAgeValues = [];

  setInterval(async () => {
    try {
      const [countryState, ageState] = await Promise.all([
        slicerCountry.getSlicerState(),
        slicerAge.getSlicerState()
      ]);

      const countryValues = countryState.filters.map(f => f.values).flat();
      const ageValues = ageState.filters.map(f => f.values).flat();

      if (JSON.stringify(countryValues) !== JSON.stringify(lastCountryValues) ||
          JSON.stringify(ageValues) !== JSON.stringify(lastAgeValues)) {

        lastCountryValues = countryValues;
        lastAgeValues = ageValues;
        console.log("🎯 Slicer change detected:", { countryValues, ageValues });

        const pages2 = await report2.getPages();
        const page2 = pages2.find(p => p.isActive) || pages2[0];
        const visuals2 = await page2.getVisuals();
        const tableVisual = visuals2.find(v => v.name === "8567ad09be521ec1295a");
        if(!tableVisual) { console.error("❌ Table visual not found in report 2"); return; }

        // Remove all filters first
        await tableVisual.updateFilters(models.FiltersOperations.RemoveAll);

        // Build and apply combined filters
        const filters = [];

        if(countryValues.length > 0) {
          filters.push({
            $schema: "http://powerbi.com/product/schema#basic",
            filterType: 1,
            target: { table: "Table", column: "Country" },
            operator: "In",
            values: countryValues,
            requireSingleSelection: false
          });
        }

        if(ageValues.length > 0) {
          filters.push({
            $schema: "http://powerbi.com/product/schema#basic",
            filterType: 1,
            target: { table: "Table", column: "Age" },
            operator: "In",
            values: ageValues,
            requireSingleSelection: false
          });
        }

        if(filters.length > 0) {
          await tableVisual.updateFilters(models.FiltersOperations.Add, filters);
          console.log("✅ Applied filters to Report 2:", filters);
        } else {
          console.log("✅ Cleared filters from Report 2 (no slicer selections)");
        }
      }
    } catch(err) { console.error(err); }
  }, 1000);
}
</script>
</body>
</html>

 

 

🔍 Code Breakdown

 

Let’s walk through what each section does:

 

🏗️ 1. HTML & Style Setup

 

<div id="reportsContainer">

  <div id="reportContainerLeft" class="reportBox"></div>

  <div id="reportContainerRight" class="reportBox"></div>

</div>

 

This sets up two boxes side by side — one for each Power BI report.
The CSS display: flex ensures they appear next to each other.

 

🔐 2. Configuration & Authentication (MSAL)

 

const clientId = "...";

const tenantId = "...";

const msalInstance = new msal.PublicClientApplication({ ... });

 

  • clientId — your Azure App’s Client ID (used for authentication)
  • tenantId — your organization’s Microsoft Entra (Azure AD) ID
  • msal.PublicClientApplication — initializes MSAL.js for sign-in

When you click “Sign In & Load Reports”, the app opens a Microsoft sign-in popup and requests a Power BI access token (https://analysis.windows.net/powerbi/api/.default).

 

📊 3. Embedding Power BI Reports

 

report1 = powerbi.embed(document.getElementById("reportContainerLeft"), { ... });

report2 = powerbi.embed(document.getElementById("reportContainerRight"), { ... });

 

Once the user is authenticated, both reports are embedded using the Power BI JavaScript SDK.
Each report is loaded via its embed URL, which includes the reportId and groupId.

 

🧾 4. Listing Slicers & Visuals

 

const visuals = await page.getVisuals();

if(visual.type === "slicer") console.log(visual.name);

 

These two buttons are diagnostic helpers:

  • “List Slicers in Report 1” logs all slicers (so you can find their IDs)
  • “List Visuals in Report 2” logs visuals like tables or charts

You’ll need these IDs to target specific visuals in the filtering logic.

 

⚙️ 5. Dual Slicer Listener

 

The most powerful part of the script.

 

setInterval(async () => {

  const countryState = await slicerCountry.getSlicerState();

  const ageState = await slicerAge.getSlicerState();

  ...

}, 1000);

 

This runs every 1 second:

  • Reads the current slicer selections
  • Compares with previous values
  • If changed → builds filters and applies them to the target visual in Report 2

🧩 6. Applying Filters to the Second Report

 

await tableVisual.updateFilters(models.FiltersOperations.RemoveAll);

await tableVisual.updateFilters(models.FiltersOperations.Add, filters);

The code:

 

  1. Clears all previous filters on the table visual
  2. Adds new filters for Country and Age columns, based on the slicer states from Report 1

The filter schema used is the Power BI Basic filter format:

{

  $schema: "http://powerbi.com/product/schema#basic",

  filterType: 1,

  target: { table: "Table", column: "Country" },

  operator: "In",

  values: ["USA", "Canada"]

}

 

🚀 Running the Code with a Python Server

 

Power BI embedding requires your app to run on a local or hosted server (not a file:// path).
You can use Python’s built-in web server to serve this HTML file.

 

Step 1 — Save as index.html

Save the code into a folder, e.g.:

C:\powerbi-dual-slicer\

 

Step 2 — Run Server

In that folder, run:

python -m http.server 5500

 

Step 3 — Open in Browser

Go to:

http://localhost:5500/index.html

 

Click Sign In & Load Reports, and once you authenticate, both reports will load.

 

🧰 Troubleshooting

Issue

Possible Fix

Authentication failed

Ensure redirect URI matches http://localhost:8000 in Azure App registration

Slicer not found

Use “List Slicers” button to check slicer IDs

Table not filtered

Verify visual ID (tableVisual.name) in Report 2

Blank screen

Must serve the file using a web server (not file://)

 

🏁 Conclusion

 

This project demonstrates a powerful pattern — cross-report communication using only JavaScript, MSAL authentication, and Power BI’s public client API.

You can extend this by:

  • Adding more slicers or visuals
  • Building multi-report dashboards

 

Recording 2025-10-15 133005.gif

 

I have attached the two reports i have used for this demo for your reference. 

Comments