- Subscribe to RSS Feed
- Mark Topic as New
- Mark Topic as Read
- Float this Topic for Current User
- Bookmark
- Subscribe
- Printer Friendly Page
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Dropdown options list is positined and scaled relatively to browser window instead of the object
Dear Community,
I'm experiencin a very annoying issue where the list of options (that appears when clicking the dropdown) is poisitioned and scaled relatively to the browser window but not to the dialogue box of the dropdown (see images).
In the image on left I window is 100% scaled and positioned in the middle. On the right I have increased window scaling to ~150% and moved it to the left. As a result of these manipulations the dropdown dialogue box moves and scales as expected, but the list of options stays unoved and unscaled.
Here is my visual.ts file:
import powerbi from "powerbi-visuals-api";
import "./../style/visual.less";
import { IconType, SVGIcons } from "./../style/svg_icons"
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import IVisual = powerbi.extensibility.visual.IVisual;
import DataView = powerbi.DataView;
import * as d3 from "d3";
import { BasicFilter, IFilterColumnTarget } from "powerbi-models";
import IFilter = powerbi.IFilter;
import FilterAction = powerbi.FilterAction;
export class Visual implements IVisual {
// Visual elements
private svgRoot: d3.Selection<SVGElement, {}, HTMLElement, any>;
private dropdowns: d3.Selection<HTMLSelectElement, {}, HTMLElement, any>[] = [];
private clearButtons: d3.Selection<HTMLButtonElement, {}, HTMLElement, any>[] = [];
// Parameters
private visualHost: powerbi.extensibility.visual.IVisualHost;
private currentDataView: DataView | null = null;
private postalCode: string = "";
private lob: string = "";
constructor(options: VisualConstructorOptions) {
this.visualHost = options.host;
this.svgRoot = d3.select(options.element).append("svg")
.style("background", "rgb(0, 82, 164)")
.style("cursor", "default");
// Dropdowns
const dropdownNames = ["Postal Code", "LOB"];
dropdownNames.forEach((name, index) => {
this.svgRoot.append("text")
.text(name + ":")
.attr("x", 10)
.attr("y", 90 + index * 40)
.style("font-size", "16px")
.style("fill", "white");
const dropdown = d3.select(options.element)
.append("select")
.attr("class", "dropdown-box")
.style("position", "absolute")
.style("top", `${70 + index * 40}px`)
.style("left", "150px")
.style("width", "138px")
.style("padding", "8px")
.style("border", "1px solid rgb(0, 122, 197)")
.style("border-radius", "6px")
.style("background", "white")
.style("color", "black")
.style("cursor", "pointer")
.on("change", (event) => this.handleDropdownChange(index, event))
.on("click", () => this.clearDropdown(index));
this.dropdowns.push(dropdown);
const defaultColor = "rgb(215,215,215)";
const highlightColor = "white"
const clearButton = d3.select(options.element)
.append("button")
.html(SVGIcons.Get_SVG_Icon(IconType.Eraser, defaultColor))
.style("width", "15px")
.style("height", "15px")
.style("position", "absolute")
.style("top", `${80 + index * 40}px`)
.style("left", "290px")
.style("padding", "0px 0px")
.style("border", "none")
.style("background", "transparent")
.style("cursor", "pointer")
.on("mouseover", function () {
d3.select(this).select("svg path").attr("fill", highlightColor);
})
.on("mouseout", function () {
d3.select(this).select("svg path").attr("fill", defaultColor);
})
.on("click", () => this.clearDropdown(index))
this.clearButtons.push(clearButton);
});
}
private handleDropdownChange(index: number, event: any): void {
const value = event.target.value;
if (index === 0) {
this.postalCode = value;
} else if (index === 1) {
this.lob = value;
}
if (this.currentDataView) {
this.applyFilter(this.currentDataView);
}
}
public update(options: VisualUpdateOptions) {
this.currentDataView = options.dataViews[0]; // Save the DataView to a class property
this.svgRoot
.attr("width", options.viewport.width)
.attr("height", options.viewport.height);
let dataView: DataView = this.currentDataView;
let table = dataView.table;
let postalCodeIndex = table.columns.findIndex(col => col.roles["PostalCode"]);
let lobIndex = table.columns.findIndex(col => col.roles["LOB"]);
if (postalCodeIndex !== -1) {
let postalCodes = Array.from(new Set(table.rows.map(row => String(row[postalCodeIndex]))));
this.populateDropdown(this.dropdowns[0], postalCodes, this.postalCode);
}
if (lobIndex !== -1) {
let lobs = Array.from(new Set(table.rows.map(row => String(row[lobIndex]))));
this.populateDropdown(this.dropdowns[1], lobs, this.lob);
}
}
private populateDropdown(dropdown: d3.Selection<HTMLSelectElement, {}, HTMLElement, any>, values: string[], selectedValue: string): void {
dropdown.selectAll("option").remove(); // Clear existing options
// Preserve existing selection
let isSelectedValueInList = values.includes(selectedValue);
// Add options dynamically
values.forEach(value => {
dropdown.append("option")
.text(value)
.attr("value", value)
.attr("selected", value === selectedValue ? "selected" : null); // Keep previous selection
});
if (!isSelectedValueInList) {
dropdown.property("value", ""); // Reset only if the selection is invalid
}
}
private applyFilter(dataView: DataView): void {
if (!this.visualHost) {
console.error("Visual Host is not initialized properly.");
return;
}
const filterValues: string[] = [];
let targets: IFilterColumnTarget[] = [];
if (this.postalCode) filterValues.push(this.postalCode);
if (this.lob) filterValues.push(this.lob);
if (dataView?.metadata?.columns) {
dataView.metadata.columns.forEach(column => {
console.log("Checking Column:", column.displayName, "Query Name:", column.queryName);
if (column.displayName === "PostalCode" && this.postalCode) {
targets.push({
table: column.queryName.split('.')[0],
column: column.displayName
});
}
if (column.displayName === "LOB" && this.lob) {
targets.push({
table: column.queryName.split('.')[0],
column: column.displayName
});
}
});
}
console.log("Targets:", targets);
console.log("Filter Values:", filterValues);
if (targets.length > 0 && filterValues.length > 0) {
const filters: IFilter[] = targets.map((target, index) =>
new BasicFilter(target, "In", [filterValues[index]]) // Use index instead of shift()
);
filters.forEach(filter => {
this.visualHost.applyJsonFilter(filter, "general", "filter", FilterAction.merge);
});
console.log("Filters Applied: ", filters);
} else {
this.visualHost.applyJsonFilter(null, "general", "filter", FilterAction.merge);
console.log("Filters Cleared.");
}
}
private clearDropdown(index: number): void {
if (index === 0) {
this.postalCode = "";
} else if (index === 1) {
this.lob = "";
}
this.dropdowns[index].property("value", "");
this.visualHost.applyJsonFilter(null, "general", "filter", FilterAction.merge);
console.log(`Dropdown ${index} cleared.`);
}
}
Thank you very much in advance!
Kind regards,
Artem
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
The issue you're facing with the dropdown options not scaling or positioning correctly relative to the dropdown dialog box, especially after zooming or adjusting the window scaling, could be a result of how the dropdown is being styled and positioned. In your current code, you're using style("position", "absolute") to position the dropdown. This can cause problems with scaling and resizing, particularly when the browser's zoom level changes, as the dropdown may not dynamically adjust relative to the container or the parent dialog box.
Possible Solutions:
-
Use Relative Positioning: Instead of using
absolute positioning for the dropdowns, you can try using
relative or removing the
position: absolute altogether. This will make the dropdowns behave more responsively, adapting to changes in the container's size.
.style("position", "relative") // Instead of "absolute"
-
Update Dropdown Position on Resize/Zoom: You can add a resize listener to adjust the position of the dropdown based on changes in the viewport or container size. This way, when the zoom changes, the dropdown can be repositioned appropriately.
For example, you can listen to the
resize event and adjust the dropdown's
top and
left properties accordingly:
window.addEventListener("resize", () => { this.updateDropdownPosition(); }); private updateDropdownPosition(): void { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Adjust dropdown positions based on viewport size or any other logic this.dropdowns.forEach((dropdown, index) => { dropdown.style("top", `${70 + index * 40}px`); dropdown.style("left", `${viewportWidth * 0.2}px`); // Example logic to adjust left position dynamically }); }
-
CSS Flexbox/Grid Layout: Another approach is to use a more flexible layout system like Flexbox or Grid to lay out the dropdowns and other elements. This would help the dropdowns scale and position more dynamically without the need for explicit
absolute positioning.
Example:
.dropdown-container { display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; } .dropdown-box { width: 138px; padding: 8px; border: 1px solid rgb(0, 122, 197); border-radius: 6px; background: white; color: black; cursor: pointer; }
In your TypeScript, wrap the dropdowns inside a container with the class
dropdown-container, and position them using Flexbox or Grid.
-
Detect "Clear Filters" Button Click: To reliably detect when the "Clear Filters" button is clicked, you've already implemented the
clearDropdown function which resets the dropdown values and clears the filters. The issue you're facing might be due to the multiple updates triggered on filter changes. To address this, you can debounce the filter change events or delay the application of the filters to ensure they don't overlap.
For example, you could add a debounce mechanism to prevent the flickering behavior by limiting how often updates are applied:
private debounceTimeout: number | null = null; private handleDropdownChange(index: number, event: any): void { const value = event.target.value; if (this.debounceTimeout) { clearTimeout(this.debounceTimeout); } this.debounceTimeout = setTimeout(() => { if (index === 0) { this.postalCode = value; } else if (index === 1) { this.lob = value; } if (this.currentDataView) { this.applyFilter(this.currentDataView); } }, 300); // Adjust the timeout as needed (in ms) }
-
CSS Transitions or Smooth Scrolling: You can also add a smooth transition effect to the dropdown's appearance and disappearance, which might reduce the flickering effect:
.dropdown-box { transition: opacity 0.3s ease-in-out; }
In Summary:
-
Switch to relative positioning or use Flexbox/Grid layout to avoid issues with absolute positioning.
-
Implement resize listeners to adjust the dropdown position dynamically.
-
Debounce the dropdown value changes to avoid multiple rapid updates causing flickering.
-
Add CSS transitions for smoother state changes and user experience.
These changes should help mitigate the flickering and improve the user experience with your custom dropdown visual in Power BI.
Did I answer your question? Mark my post as a solution! Appreciate your Kudos !!
-
-
-
-
-
-
-
-

Helpful resources
Join our Fabric User Panel
This is your chance to engage directly with the engineering team behind Fabric and Power BI. Share your experiences and shape the future.
Power BI Monthly Update - June 2025
Check out the June 2025 Power BI update to learn about new features.
