Blog Post

Power BI Community Blog
7 MIN READ

Real Time Cross-Report Filtering Using JavaScript embedded analytics

kushanNa's avatar
kushanNa
Icon for Super User rankSuper User
11 months ago

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/[email protected]/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

 

 

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

Updated 11 months ago
Version 1.0