Don't miss your chance to take the Fabric Data Engineer (DP-700) exam on us!
Learn moreWe've captured the moments from FabCon & SQLCon that everyone is talking about, and we are bringing them to the community, live and on-demand. Starts on April 14th. Register now
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:
💻 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({ ... });
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:
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:
🧩 6. Applying Filters to the Second Report
await tableVisual.updateFilters(models.FiltersOperations.RemoveAll);
await tableVisual.updateFilters(models.FiltersOperations.Add, filters);
The code:
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:
I have attached the two reports i have used for this demo for your reference.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.