Add interactivity into visual by Power BI visuals selections

Power BI provides two ways to interact with visuals - selecting and filtering. The following example demonstrates how to select an item from one visual and notify the other visuals in the report about the new selection state.

The interface corresponds to a Selection object:

export interface ISelectionId {
    equals(other: ISelectionId): boolean;
    includes(other: ISelectionId, ignoreHighlight?: boolean): boolean;
    getKey(): string;
    getSelector(): Selector;
    getSelectorsByColumn(): SelectorsByColumn;
    hasIdentity(): boolean;
}

Use the selection manager to select data points

The visual host object provides a method for creating an instance of the selection manager. The selection manager has a corresponding method for each of the following actions:

  • Select
  • Clear the selection
  • Show the context menu
  • Store the current selections
  • Check the selection state

Create an instance of the selection manager

To use the selection manager, create the instance of a selection manager. Usually, visuals create a selection manager instance in the constructor section of the visual object.

export class Visual implements IVisual {
    private target: HTMLElement;
    private host: IVisualHost;
    private selectionManager: ISelectionManager;
    // ...
    constructor(options: VisualConstructorOptions) {
        this.host = options.host;
        // ...
        this.selectionManager = this.host.createSelectionManager();
    }
    // ...
}

Create an instance of the selection builder

When the selection manager instance is created, you need to create selections for each data point of the visual. The visual host object's createSelectionIdBuilder method generates a selection for each data point. This method returns an instance of the object with interface powerbi.visuals.ISelectionIdBuilder:

export interface ISelectionIdBuilder {
    withCategory(categoryColumn: DataViewCategoryColumn, index: number): this;
    withSeries(seriesColumn: DataViewValueColumns, valueColumn: DataViewValueColumn | DataViewValueColumnGroup): this;
    withMeasure(measureId: string): this;
    withMatrixNode(matrixNode: DataViewMatrixNode, levels: DataViewHierarchyLevel[]): this;
    withTable(table: DataViewTable, rowIndex: number): this;
    createSelectionId(): ISelectionId;
}

This object has corresponding methods to create selections for different types of data view mappings.

Note

The methods withTable and withMatrixNode were introduced on API 2.5.0 of the Power BI visuals. If you need to use selections for table or matrix data view mappings, update to API version 2.5.0 or higher.

Create selections for categorical data view mapping

Let's review how selections represent categorical data view mapping for a sample semantic model:

Manufacturer Type Value
Chrysler Domestic Car 28883
Chrysler Domestic Truck 117131
Chrysler Import Car 0
Chrysler Import Truck 6362
Ford Domestic Car 50032
Ford Domestic Truck 122446
Ford Import Car 0
Ford Import Truck 0
GM Domestic Car 65426
GM Domestic Truck 138122
GM Import Car 197
GM Import Truck 0
Honda Domestic Car 51450
Honda Domestic Truck 46115
Honda Import Car 2932
Honda Import Truck 0
Nissan Domestic Car 51476
Nissan Domestic Truck 47343
Nissan Import Car 5485
Nissan Import Truck 1430
Toyota Domestic Car 55643
Toyota Domestic Truck 61227
Toyota Import Car 20799
Toyota Import Truck 23614

The visual uses the following data view mapping:

{
    "dataRoles": [
        {
            "displayName": "Columns",
            "name": "columns",
            "kind": "Grouping"
        },
        {
            "displayName": "Rows",
            "name": "rows",
            "kind": "Grouping"
        },
        {
            "displayName": "Values",
            "name": "values",
            "kind": "Measure"
        }
    ],
    "dataViewMappings": [
        {
            "categorical": {
                "categories": {
                    "for": {
                        "in": "columns"
                    }
                },
                "values": {
                    "group": {
                        "by": "rows",
                        "select": [
                            {
                                "for": {
                                    "in": "values"
                                }
                            }
                        ]
                    }
                }
            }
        }
    ]
}

In the preceding example, Manufacturer is columns and Type is rows. A series is created by grouping values by rows (Type).

The visual should be able to slice data by Manufacturer or Type.

For example, if a user selects Chrysler by Manufacturer, other visuals should show the following data:

Manufacturer Type Value
Chrysler Domestic Car 28883
Chrysler Domestic Truck 117131
Chrysler Import Car 0
Chrysler Import Truck 6362

When the user selects Import Car by Type (selects data by series), the other visuals should show the following data:

Manufacturer Type Value
Chrysler Import Car 0
Ford Import Car 0
GM Import Car 197
Honda Import Car 2932
Nissan Import Car 5485
Toyota Import Car 20799

Screenshot that shows the visual with selections.

To display sliced data, fill the visual's data baskets as follows:

Screenshot that shows visual's data baskets.

In the preceding example, Manufacturer is category (columns), Type is series (rows), and Sales is Values for series.

Note

Values are required for displaying a series because, according to the data view mapping, Values are grouped by Rows data.

Create selections for categories

// categories
const categories = dataView.categorical.categories;

// create label for 'Manufacturer' column
const p = document.createElement("p") as HTMLParagraphElement;
p.innerText = categories[0].source.displayName.toString();
this.target.appendChild(p);

// get count of category elements
const categoriesCount = categories[0].values.length;

// iterate all categories to generate selection and create button elements to use selections
for (let categoryIndex = 0; categoryIndex < categoriesCount; categoryIndex++) {
    const categoryValue: powerbi.PrimitiveValue = categories[0].values[categoryIndex];

    const categorySelectionId = this.host.createSelectionIdBuilder()
        .withCategory(categories[0], categoryIndex) // we have only one category (only one `Manufacturer` column)
        .createSelectionId();
    this.dataPoints.push({
        value: categoryValue,
        selection: categorySelectionId
    });
    console.log(categorySelectionId);

    // create button element to apply selection on click
    const button = document.createElement("button") as HTMLButtonElement;
    button.value = categoryValue.toString();
    button.innerText = categoryValue.toString();
    button.addEventListener("click", () => {
        // handle click event to apply correspond selection
        this.selectionManager.select(categorySelectionId);
    });
    this.target.appendChild(button);
}

In the preceding sample code, we iterate through all categories. In each iteration, we call createSelectionIdBuilder to create the next selection for each category by calling the withCategory method of the selection builder. The createSelectionId method is used as a final method to return the generated selection object.

In the withCategory method, we pass the column of category, in the sample, its Manufacturer, and the index of category element.

Create selections for series

// get groupped values for series
const series: powerbi.DataViewValueColumnGroup[] = dataView.categorical.values.grouped();

// create label for 'Type' column
const p2 = document.createElement("p") as HTMLParagraphElement;
p2.innerText = dataView.categorical.values.source.displayName;
this.target.appendChild(p2);

// iterate all series to generate selection and create button elements to use selections
series.forEach( (ser: powerbi.DataViewValueColumnGroup) => {
    // create selection id for series
    const seriesSelectionId = this.host.createSelectionIdBuilder()
        .withSeries(dataView.categorical.values, ser)
        .createSelectionId();

    this.dataPoints.push({
        value: ser.name,
        selection: seriesSelectionId
    });

    // create button element to apply selection on click
    const button = document.createElement("button") as HTMLButtonElement;
    button.value =ser.name.toString();
    button.innerText = ser.name.toString();
    button.addEventListener("click", () => {
        // handle click event to apply correspond selection
        this.selectionManager.select(seriesSelectionId);
    });
    this.target.appendChild(button);
});

Create selections for table data view mapping

The following example shows table data view mapping:

{
    "dataRoles": [
        {
            "displayName": "Values",
            "name": "values",
            "kind": "GroupingOrMeasure"
        }
    ],
    "dataViewMappings": [
        {
            "table": {
                "rows": {
                    "for": {
                        "in": "values"
                    }
                }
            }
        }
    ]
}

To create a selection for each row of table data view mapping, call the withTable method of selection builder.

public update(options: VisualUpdateOptions) {
    const dataView = options.dataViews[0];
    dataView.table.rows.forEach((row: DataViewTableRow, rowIndex: number) => {
        this.target.appendChild(rowDiv);
        const selection: ISelectionId = this.host.createSelectionIdBuilder()
            .withTable(dataView.table, rowIndex)
            .createSelectionId();
    }
}

The visual code iterates the rows of the table and each row calls the withTable table method. Parameters of the withTable method are the table object and the index of the table row.

Create selections for matrix data view mapping

public update(options: VisualUpdateOptions) {
    const host = this.host;
    const rowLevels: powerbi.DataViewHierarchyLevel[] = dataView.matrix.rows.levels;
    const columnLevels: powerbi.DataViewHierarchyLevel[] = dataView.matrix.rows.levels;

    // iterate rows hierarchy
    nodeWalker(dataView.matrix.rows.root, rowLevels);
    // iterate columns hierarchy
    nodeWalker(dataView.matrix.columns.root, columnLevels);

    function nodeWalker(node: powerbi.DataViewMatrixNode, levels: powerbi.DataViewHierarchyLevel[]) {
        const nodeSelection = host.createSelectionIdBuilder().withMatrixNode(node, levels);

        if (node.children && node.children.length) {
            node.children.forEach(child => {
                nodeWalker(child, levels);
            });
        }
    }
}

In the sample, nodeWalker recursively calls each node and child node.

nodeWalker creates a nodeSelection object on each call. Each nodeSelection represents a selection of corresponding nodes.

Select data points to slice other visuals

In this example, we created a click handler for button elements. The handler calls the select method of the selection manager and passes the selection object.

button.addEventListener("click", () => {
    // handle click event to apply correspond selection
    this.selectionManager.select(categorySelectionId);
});

The interface of the select method:

interface ISelectionManager {
    // ...
    select(selectionId: ISelectionId | ISelectionId[], multiSelect?: boolean): IPromise<ISelectionId[]>;
    // ...
}

The select method can accept an array of selections. This allows your visual to have several data points selected at once. The second parameter, multiSelect, is responsible for multi-selections. If multiSelect is true, Power BI doesn't clear the previous selection state when it applies the current selection. If the value is false, the previous selection is overwritten.

A typical example of using multiSelect is handling the Ctrl button state on a click event. When the Ctrl button is held down, you can select more than one object.

button.addEventListener("click", (mouseEvent) => {
    const multiSelect = (mouseEvent as MouseEvent).ctrlKey;
    this.selectionManager.select(seriesSelectionId, multiSelect);
});