Reply
Alkatraz
Frequent Visitor

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).

 

Screenshot 2025-03-25 192356.jpgImage Placeholder.jpg

 

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

1 REPLY 1
johnbasha33
Super User
Super User

 

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:

  1. 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"
  2. 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
        });
    }
  3. 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.

  4. 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)
    }
  5. 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:

    1. Switch to relative positioning or use Flexbox/Grid layout to avoid issues with absolute positioning.

    2. Implement resize listeners to adjust the dropdown position dynamically.

    3. Debounce the dropdown value changes to avoid multiple rapid updates causing flickering.

    4. 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 !!

    5.  

    6.  

    7.  

@Alkatraz

avatar user

Helpful resources

Announcements
Join our Fabric User Panel

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.

June 2025 Power BI Update Carousel

Power BI Monthly Update - June 2025

Check out the June 2025 Power BI update to learn about new features.

June 2025 community update carousel

Fabric Community Update - June 2025

Find out what's new and trending in the Fabric community.

Top Solution Authors (Last Month)
Top Kudoed Authors (Last Month)