Join us at FabCon Atlanta from March 16 - 20, 2026, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.
Register now!Get Fabric Certified for FREE during Fabric Data Days. Don't miss your chance! Request now
Hello,
I am developing a organizational Power BI custom visual for my company that needs to detect the user’s row selection from a Power BI Table or Matrix visual.
I am experiencing inconsistent behavior in the DataView highlights and identities provided to my custom visual.
My custom visual reads row selection using:
categorical.values[x].highlights[]
categorical.categories[0].identity[] (fallback)
This works correctly only in some situations. However, when:
the Table/Matrix visual contains many columns,
or when new categorical columns are added,
or when some rows contain null values,
or when the model grows in size,
Power BI stops sending highlight arrays and also sends placeholder identities instead of real row identities.
My visual is supposed to work as commenting tool:
A user clicks a row in table or matrix → the visual must detect the selected row → user enters a comment for that row.
This functionality worked previously, and still works if the Table contains only a few columns.
When more columns are added, or data grows, highlights and identities suddenly disappear.
function deriveSelectedKey(dv?: powerbi.DataView): CellSelection {
const cat = dv?.categorical;
const cats = cat?.categories ?? [];
if (!cats.length) return { state: "no-categories" };
const rowCount = cats[0].values.length;
// collect value columns except the CurrentUser (same as before)
const values = cat?.values?.filter(
(val) => val.source.displayName !== "CurrentUser"
);
// --- 1) Try highlights (original method) ---
const selectedIndicesFromHighlights: number[] = [];
if (
values?.length &&
values.some((v) => Array.isArray((v as any).highlights))
) {
for (let r = 0; r < rowCount; r++) {
const hl = values.some((v) => (v as any).highlights?.[r] != null);
if (hl) selectedIndicesFromHighlights.push(r);
}
}
// If highlights found -> use them (preserves your original behavior when available)
if (selectedIndicesFromHighlights.length > 0) {
// handle single/multiple
if (selectedIndicesFromHighlights.length === 1) {
const item = buildSelectionItemFromCats(
selectedIndicesFromHighlights[0],
cats
);
return {
state: "single",
index: item.index,
key: item.key,
parts: item.parts,
};
}
const items = selectedIndicesFromHighlights.map((i) =>
buildSelectionItemFromCats(i, cats)
);
return { state: "multiple", items };
}
// --- 2) Try identities (fallback) ---
// identity field is not strongly typed in typings; treat as any[]
const mainCat = cats[0] as any;
const identities: any[] | undefined = mainCat.identity as any[] | undefined;
// If no identities and no highlights, fallback:
if ((!identities || identities.length === 0) && rowCount === 1) {
// single-row table -> default to row 0 (same fallback as before)
const item = buildSelectionItemFromCats(0, cats);
return {
state: "single",
index: item.index,
key: item.key,
parts: item.parts,
};
}
// If identities exist but look like placeholders (0,1,2,...) then they are not useful:
const identitiesArePlaceholders = areIdentitiesPlaceholders(identities);
// If identities are placeholders and we had no highlights -> we cannot detect selection reliably
if (identitiesArePlaceholders) {
// if there is exactly one row, pick it
if (rowCount === 1) {
const item = buildSelectionItemFromCats(0, cats);
return {
state: "single",
index: item.index,
key: item.key,
parts: item.parts,
};
}
// otherwise return none: this matches the observed behavior where PB doesn't send real IDs/highlights
return { state: "none" };
}
... rest of code
Hi @Q-Click ,
The behavior you’re observing is expected due to how Power BI handles highlights and row identities in Table/Matrix visuals. Highlights (categorical.values[x].highlights) are only sent reliably for small tables or when a single selection exists. For larger tables, multiple columns, or rows containing nulls, Power BI may omit highlights or send placeholder identities (0,1,2…), which cannot be used to detect actual row selection.
To address this issue, add a unique key column (e.g., RowID) as the first categorical column in your table or matrix and rely on it to detect row selection instead of highlights, so selections remain reliable even if identities are placeholders or highlights are missing. For interactive visuals, you can optionally use ISelectionManager to register selections more consistently. Keep your fallback logic for single-row tables to default to row 0. This approach ensures your visual reliably detects selections even with many columns, added categorical fields, null values, or larger datasets, eliminating dependence on inconsistent highlights and placeholder identities and maintaining full commenting functionality.
Hi @Q-Click ,
I hope the information provided above assists you in resolving the issue. If you have any additional questions or concerns, please do not hesitate to contact us. We are here to support you and will be happy to help with any further assistance you may need.
Hi,
@v-sshirivolu Thanks for your response!
I have a follow-up question regarding the “unique RowID column” solution.
Would adding a RowID column truly solve the issue, or is it only a partial workaround?
More specifically:
Will this approach still remain reliable if the table/matrix becomes very wide or contains many categorical columns?
The selection/highlighting interruption that I’m seeing appears only when the dataset becomes large enough or when additional columns are introduced.
How exactly should a custom visual consume this RowID column?
Since the recommended approach is to avoid relying on highlights[], and the Selection Manager is only suggested as an optional alternative, I’m trying to understand how adding a RowID changes the behavior of how Power BI sends filter/selection context to custom visuals.
Does Power BI guarantee that a RowID used as a single unique category will always produce stable identity or filtering context for the custom visual, regardless of dataset size or number of columns?
I ask this because previously the selection context simply stopped being passed once the table became too large, even though no RowID was involved.
I would really appreciate clarification on how adding this RowID is supposed to help technically, and whether it avoids the limitations that cause Power BI to drop highlight arrays or identity information when the underlying table becomes large.
Thanks again!
Hi ,
Custom Visual: highlights/selection stop working when Table/Matrix gets big
You’re not imagining it. What you’re seeing is a mix of (a) how highlights are delivered to custom visuals, (b) data reduction/windowing, and (c) rows that don’t carry real identities (nulls/totals/rolled-up rows).
What’s actually happening
Highlights only arrive when your visual opted in
With "supportsHighlight": true, the host may send categorical.values[i].highlights (same length as values; non-selected rows are null). That array is only guaranteed for cross-visual highlight scenarios, not for your own selection, and not in all table/matrix states. Microsoft Learn
Your visual probably isn’t receiving the selected row because of data reduction
By default, custom visuals receive at most ~1000 rows per query window (top reduction). If the user selects a row that’s outside the window your visual requested, the host can’t line up the selected index → the highlights array is omitted (or doesn’t mark any of your current window rows). Use a bigger dataReductionAlgorithm count and/or implement fetchMoreData to page until the selected row falls into your window. Microsoft Learn+2GitHub+2
Some rows don’t have real identities
Table/Matrix can surface totals, blanks, or grouped rows. Those can show placeholders (or no identity at all), so your fallback on categorical.categories[0].identity[] won’t map back to a real selection. (This is why things work on “small/simple” tables but fail once you add columns, nulls, or groupings.) There isn’t a single doc line that calls it “placeholders,” but identity issues with table mapping and selection are a well-trodden pain point in the SDK/GitHub threads. coacervo+1
Practical fixes (what I’d change)
Raise your row window + implement paging
In capabilities.json, bump the window:
"dataViewMappings": [{
"categorical": {
"dataReductionAlgorithm": { "top": { "count": 30000 } }
}
}]
Handle fetchMoreData() when a selection occurs but your current window shows no highlighted rows. Keep appending until you either see the selected row or the host returns “no more data.” Microsoft Learn
Add a stable surrogate key and bind it in both visuals
Put a not-null RowKey (e.g., surrogate id) in the Table/Matrix and also bind the same field into your custom visual’s categories. This gives the engine a clean join path for highlights/identities even when the model grows, and avoids null/total rows breaking identity mapping. (This is the most reliable long-term pattern.)
Treat highlights as cross-visual only; own your selection via Selection Manager
Keep your “read highlights” path only when highlights exists.
For your own clicks, store/restore ISelectionIds with ISelectionManager so a re-update() doesn’t wipe your state. (Highlights are not the echo channel for your own selection.) Microsoft Learn+1
Defend against identity-less rows
If a selected Table/Matrix row has no identity (blank/total), show a friendly “cannot comment on totals/blank rows” message or require the RowKey field.
Optionally filter the user’s Table to exclude totals/blank GTM/keys to guarantee identities.
Consider table mapping if you truly work row-by-row
If your visual is logically “row” oriented, table dataViewMapping can be simpler to reason about than categorical when you start paging. (Still doesn’t fix identity for totals/nulls, but it makes row scanning straightforward.) coacervo
// On click in Table/Matrix (cross-visual): your visual receives update()
const hasHighlights = values?.some(v => Array.isArray((v as any).highlights));
if (!hasHighlights) {
// Selected row might be outside our window → try to page
if (await this.host.fetchMoreData()) return; // host will call update() again
}
// When we *do* have highlights, compute selected indices
const selectedIdx = rows.filter((r,i) => values.some(v => v.highlights?.[i] != null));
// If still none: either totals/blank identity or selection not in our data
// → fall back to RowKey (if you bound it) or show a friendly message.
Did I answer your question? Mark my post as a solution! Appreciate your Kudos !!
Thank you very much for the detailed breakdown.
Let me summarize your proposed solution to verify that I fully understand the mechanics behind it:
Highlights are not guaranteed
They are only reliably sent in cross-visual scenarios, and only when the selected row is inside the current data window delivered to the custom visual.
If the selected row is outside the window, or if Power BI decides to reduce the payload, highlights[] will be missing or truncated.
Data reduction / windowing is likely the root cause
My custom visual receives only a window of ~1000 rows by default.
When the user selects a row outside that window, Power BI cannot align indices → the highlight array is dropped.
To fix this, I should increase the dataReductionAlgorithm.top.count and implement fetchMoreData() when I detect no highlights.
Identity issues occur when the table surface includes totals/null/grouped rows
In these cases, the Table/Matrix does not send real identities, only placeholders, which explains why my fallback identity logic stops working when the model becomes wide or contains blanks.
A stable surrogate RowKey is recommended
Adding a guaranteed non-null RowKey (bound both in the Table/Matrix and in my custom visual) gives Power BI a consistent join path that reduces identity mismatches and avoids the null/total cases breaking selection propagation.
Highlights should be treated as cross-visual only; not as a mechanism for my own internal selection
For my own state I should use ISelectionManager, because highlights are not meant to be the "echo" channel for my visual’s own selection state.
Paging logic should fire when I detect “no highlights”
If highlights[] is missing, it may simply mean the selected row is not yet in the window → so the correct next step is host.fetchMoreData() until the selection becomes visible or I exhaust all pages.
Am I correct that adding a RowKey does not itself guarantee that highlights will always be sent, but instead reduces cases where Power BI collapses identities or drops highlights due to null/total rows?
Regarding data reduction:
Is the main expectation that once I set a larger top.count (e.g., 30,000) and implement fetchMoreData(), highlight disappearance should no longer occur unless the dataset exceeds that expanded window?
For the RowKey:
Should I treat this RowKey as the only meaningful category for selection detection inside my custom visual, and ignore all other category columns?
If the user selects a row that has no real identity (totals, nulls, rolled-up rows), is the recommended behavior to show a message like "Cannot comment on total/blank rows" rather than trying to derive fallback selection?
Finally, is it correct that none of this guarantees "unlimited" reliability for Table/Matrix selection, but implementing:
a surrogate RowKey,
higher data reduction window,
fetchMoreData paging,
highlight-as-cross-visual-only,
yields the most stable approach possible within the current Power BI custom visual model?
Thank you again for sharing these insights!
Check out the November 2025 Power BI update to learn about new features.
Advance your Data & AI career with 50 days of live learning, contests, hands-on challenges, study groups & certifications and more!
| User | Count |
|---|---|
| 5 | |
| 3 | |
| 2 | |
| 1 | |
| 1 |