Supplies are limited. Contact info@espc.tech right away to save your spot before the conference sells out.
Get your discountScore big with last-minute savings on the final tickets to FabCon Vienna. Secure your discount
Issue: Custom Power BI visual (date picker slicer) cannot properly integrate with Power Query parameters and DAX functions like
SELECTEDVALUE()
and
VALUES()
.
Context:
Created a custom Power BI visual that functions as a date picker slicer
The visual is intended to work with Power Query parameters to filter data
When users select a date in the custom visual, it should update the parameter value
However,
SELECTEDVALUE(column)
VALUES(column)
BLANK()
Interestingly,
ISFILTERED(column)
TRUE
Expected Behavior:
Built-in Power BI slicers work perfectly with parameters - when a user selects a value,
SELECTEDVALUE()
Custom visual should behave similarly to built-in slicers
Used
host.applyJsonFilter()
Tried different filter operators (
In
Is
Experimented with various date formats (ISO strings, original date strings)
Used both
FilterAction.merge
FilterAction.replace
Added
requireSingleSelection: true
Implemented
ISelectionManager.select()
Created proper
ISelectionId
createSelectionIdBuilder().withCategory()
Ensured single selection by calling
clear()
select()
Mapped date values to selection IDs using consistent date formatting
Combined both JSON Filter and Selection Manager methods
Applied JSON filters for Power Query parameter integration
Used Selection Manager for DAX function compatibility
Synchronized date formats between both approaches
Ensured consistent date formatting across all methods
Used both ISO format (
YYYY-MM-DD
Handled different input date types (string, number, Date objects)
Tested different filter schemas and configurations
Experimented with logical operators and condition structures
Tried multiple filter targets and naming conventions
What Works:
Power Query parameters receive filter values correctly
ISFILTERED()
TRUE
Visual filtering functionality works as expected
Date selection and UI interaction work properly
What Doesn't Work:
SELECTEDVALUE()
BLANK()
VALUES()
DAX functions cannot access the selection context created by the custom visual
The issue appears to be a fundamental limitation of Power BI's custom visual architecture. Built-in slicers have special privileges and mechanisms to create proper DAX selection context, while custom visuals may be restricted in their ability to fully replicate this behavior. The custom visual can create filter context (hence
ISFILTERED()
works) but cannot establish the single-value selection context required by
SELECTEDVALUE()
.
This suggests that custom visuals and built-in slicers operate through different mechanisms in Power BI's filtering and selection system, with built-in components having deeper integration with the DAX evaluation engine.
Hi
Short version: custom visuals can push filter context (so ISFILTERED() goes TRUE), but they don’t get the same “slicer-privileged” path that guarantees a single-value selection context for DAX.
SELECTEDVALUE() only returns a value when the engine truly sees exactly one value in context; otherwise it returns BLANK—even if your visual UI shows one date.
What to check/fix:
Target the exact model column
Make sure your applyJsonFilter targets the same column you use in DAX, not a display/derived field. Use a Basic (or Advanced) filter with a single value and
isInverted:false. Doc: Visual Filter API. Microsoft Learn
Bind it via capabilities (not just ad-hoc)
In capabilities.json, include a
general.filter property so the filter is persisted and recognized as a visual filter:
"objects": { "general": { "properties": { "filter": { "type": { "filter": {} } } } } }
(And then call host.applyJsonFilter(filter, "general", "filter", powerbi.FilterAction.merge).) Example patterns are in the Sample Slicer. GitHubMicrosoft Learn
Prove whether it’s single-select from DAX’s point of view
Create:
HowMany = COUNTROWS(VALUES('DimDate'[Date]))
Picked = SELECTEDVALUE('DimDate'[Date])
If HowMany ≠ 1,
Picked will be BLANK. That means your filter didn’t collapse to one value in the model (commonly because of hidden hierarchies, formatting mismatches, wrong target column, or relationships).
Avoid date hierarchies / auto date-time
Make sure you’re filtering the base date column (type Date) — not the auto date hierarchy or a formatted text field. Hierarchies often lead to multiple values in context. Several threads show SELECTEDVALUE coming back blank when a hierarchy or mismatched column is involved. Microsoft Fabric Community+2Microsoft Fabric Community+2
Relationships must propagate to the measure’s table
If your date column sits in a different table and there’s no (active) relationship, the filter won’t reach the measure. Check the model diagram.
Don’t expect applyJsonFilter to filter your own visual’s dataView
By design, calling applyJsonFilter updates the report filter state; other visuals will see it, but your visual’s current
dataView won’t retro-filter in memory. Request an update (
host.refresh() or wait for update via
onDataChanged) and base logic on the model column, not your internal array. This behavior is highlighted in dev Q&A. Stack Overflow
SelectionManager ≠ filter contextselectionManager.select() is for cross-highlight and selections, not guaranteed single-value filter context for DAX. Treat SelectionManager as UI affordance; use the Filter API to create the model filter. Microsoft Learn
Declare a categorical data role for the date column.
Persist the filter in general.filter.
Apply a Basic filter with a single ISO date value against 'DimDate'[Date].
Validate with COUNTROWS(VALUES('DimDate'[Date])) = 1.
Docs and sample: Filter API and Sample Slicer’s Advanced Filter usage. Microsoft LearnGitHub
Bottom line: there isn’t a secret “RLS-like” privilege you can tap from a custom visual. You can get SELECTEDVALUE() to behave, but only when the filter you apply collapses the model to exactly one value on the real column, with relationships and hierarchies set correctly.
Did I answer your question? Mark my post as a solution! Appreciate your Kudos !!
Power Query parameters won’t feed DAX context
PQ parameters are evaluated at refresh/mashup time. They don’t create row context for measures. Your “parameter updates” can work, but DAX SELECTEDVALUE() won’t read them.
If you want a working baseline, replicate the Sample Slicer pattern:
@lulalagulu
Thank you for answering me. I sincerely appreciate it. I developed the visual as a calendar picker to work with power query M parameter. It worked well with M parameter. Here is the code:
export class Visual implements IVisual {
private target: HTMLElement;
private host: IVisualHost;
private selectionManager: ISelectionManager;
private formattingSettingsService: FormattingSettingsService;
private formattingSettings: VisualFormattingSettingsModel;
private inputContainer: HTMLDivElement;
private dateInput: HTMLInputElement;
private currentDataView: DataView | null; // 用于存储最新的数据视图
private dataPoint: Map<string, ISelectionId> = new Map();
constructor(options: VisualConstructorOptions) {
this.target = options.element;
this.host = options.host;
this.formattingSettingsService = new FormattingSettingsService();
this.selectionManager = this.host.createSelectionManager();
// Clear existing content
this.target.innerHTML = '';
// 创建UI元素 (输入框和图标)
this.createUIElements();
}
private createUIElements(): void {
const label = document.createElement('div');
label.className = 'date-label';
label.textContent = '日期';
label.style.fontWeight = 'bold';
label.style.marginBottom = '5px';
this.target.appendChild(label);
this.inputContainer = document.createElement('div');
this.inputContainer.className = 'input-container';
this.inputContainer.style.display = 'flex';
this.inputContainer.style.alignItems = 'center';
this.inputContainer.style.border = '1px solid #ccc';
this.inputContainer.style.borderRadius = '4px';
this.inputContainer.style.padding = '4px 8px';
this.inputContainer.style.backgroundColor = 'white';
this.target.appendChild(this.inputContainer);
this.dateInput = document.createElement('input');
this.dateInput.type = 'text';
this.dateInput.readOnly = true;
this.dateInput.placeholder = 'YYYY-MM-DD';
this.dateInput.style.border = 'none';
this.dateInput.style.outline = 'none';
this.dateInput.style.width = '100%';
this.dateInput.style.fontSize = '14px';
this.inputContainer.appendChild(this.dateInput);
const dateIcon = document.createElement('div');
dateIcon.innerHTML = '📅';
dateIcon.style.cursor = 'pointer';
dateIcon.style.marginLeft = '4px';
dateIcon.addEventListener('click', (event) => {
event.stopPropagation();
this.openCalendarDialog(); // 调用打开对话框的方法
});
this.inputContainer.appendChild(dateIcon);
}
private applyDateFilter(date: string): void {
if (!this.host || !this.host.applyJsonFilter) return;
// 获取或确定要筛选的表和列
let tableName: string | undefined;
let columnName: string | undefined;
if (this.currentDataView?.metadata?.columns) {
const dateColumn = this.currentDataView.metadata.columns.find(col => col.roles && col.roles['Date']);
if (dateColumn?.queryName) {
[tableName, columnName] = dateColumn.queryName.split('.');
}
}
if (!tableName || !columnName) return;
const filter = {
$schema: "https://powerbi.com/product/schema#basic",
target: {
table: tableName,
column: columnName
},
filterType: models.FilterType.Basic,
operator: "In",
values: [ new Date(date).toISOString()],
requireSingleSelection : true
};
this.host.applyJsonFilter(filter, "general", "filter", powerbi.FilterAction.merge);
}
private getDateColumnRange(): { minDate: Date, maxDate: Date } {
const defaultRange = {
minDate: new Date(1990, 1, 1),
maxDate: new Date()
};
if (!this.currentDataView || !this.currentDataView.categorical) {
return defaultRange;
}
const dates = this.currentDataView.categorical.categories![0].values
.map(row => {
const value = row;
// 处理不同类型的日期值
if (typeof value === 'string') {
return new Date(value);
} else if (typeof value === 'number') {
return new Date(value);
} else if (value instanceof Date) {
return value;
}
return null;
})
.filter((date): date is Date => date !== null && !isNaN(date.getTime()));
if (dates.length === 0) {
return defaultRange;
}
dates.sort((a, b) => a.getTime() - b.getTime());
return {
minDate: dates[0],
maxDate: dates[dates.length - 1]
};
}
private handleDialogResult(result: ModalDialogResult, targetElement: HTMLElement, sManager: ISelectionManager, dataPoint: Map<string, ISelectionId>) {
if (result.actionId === DialogAction.OK) {
const resultState = <DatePickerDialogResult>result.resultState;
const selectedDate = new Date(resultState.date);
let formatted_date = formatDate(selectedDate);
if (targetElement instanceof HTMLInputElement) {
targetElement.value = formatted_date;
} else {
targetElement.textContent = formatted_date;
}
this.applyDateFilter(formatted_date);
// let selectedId = dataPoint.get(formatted_date);
// if (selectedId) {
// sManager.select(selectedId);
// }
}
}
private openCalendarDialog(): void {
const dateRange = this.getDateColumnRange();
let startDate = new Date();
if (startDate < dateRange.minDate) {
startDate = dateRange.minDate;
} else if (startDate > dateRange.maxDate) {
startDate = dateRange.maxDate
}
const initialDialogState = {
startDate: startDate,
minDate: dateRange.minDate,
maxDate: dateRange.maxDate
};
const position = {
type: VisualDialogPositionType.RelativeToVisual,
left: 0,
top: 60
};
const size = { width: 360, height: 300 };
const dialogOptions = {
actionButtons: dialogActionsButtons,
size: size,
position: position,
title: ""
};
// const initialState: { selectedDate?: string } = {};
if (this.dateInput.value) {
// 如果输入框有日期,作为初始值传给对话框
initialDialogState.startDate = new Date(this.dateInput.value); // this.formatDate(selectedDate) -> YYYY/MM/DD
}
this.host.openModalDialog(
DatePickerDialog.id,
dialogOptions,
initialDialogState // 传递初始日期给对话框
).then(result => {
console.log("Dialog closed with result:", result);
this.handleDialogResult(result, this.dateInput, this.selectionManager, this.dataPoint);
}).catch(error => {
console.error("Error opening date picker dialog:", error);
});
}
public destroy(): void {
}
public update(options: VisualUpdateOptions) {
console.log("filter: " + JSON.stringify(options.jsonFilters, null, 4));
this.handleDataView(options.dataViews);
}
private handleDataView(dataViews: DataView[]) {
if (!dataViews || dataViews.length == 0) {
this.currentDataView = null;
return;
}
//update the current date view
this.currentDataView = dataViews[0];
if (this.currentDataView.categorical && this.currentDataView.categorical.categories && this.currentDataView.categorical.categories.length > 0) {
this.updateSelection(this.currentDataView.categorical);
} else {
this.dataPoint.clear();
this.dateInput.value = '';
}
//sync the formattingSettings
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(
VisualFormattingSettingsModel,
this.currentDataView
);
this.updateFormatting();
}
private updateSelection(currentCategorical: powerbi.DataViewCategorical) {
const categories = currentCategorical.categories;
// 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();
//console.log(categorySelectionId);
let pearl: Date | null = null;
if (typeof categoryValue === 'string') {
pearl = new Date(categoryValue);
} else if (typeof categoryValue === 'number') {
pearl = new Date(categoryValue);
} else if (categoryValue instanceof Date) {
pearl = categoryValue;
}
let key: string = 'null';
if (pearl !== null && !isNaN(pearl.getTime())) {
key = formatDate(pearl);
}
this.dataPoint.set(key, categorySelectionId);
}
}
private updateFormatting(): void {
const labelSettings = this.formattingSettings.labelCard;
const inputSettings = this.formattingSettings.inputCard;
// 更新标签样式
const label = this.target.querySelector('.date-label') as HTMLElement;
if (label) {
if (labelSettings.show.value) {
label.style.display = "block";
label.textContent = labelSettings.text.value;
label.style.fontFamily = labelSettings.fontFamily.value;
label.style.fontSize = `${labelSettings.fontSize.value}px`;
label.style.color = labelSettings.fontColor.value.value;
} else {
label.style.display = "none";
}
}
if (this.dateInput) {
this.dateInput.style.fontFamily = inputSettings.fontFamily.value;
this.dateInput.style.fontSize = `${inputSettings.fontSize.value}px`;
this.dateInput.style.color = inputSettings.fontColor.value.value;
}
}
public getFormattingModel(): powerbi.visuals.FormattingModel {
return this.formattingSettingsService.buildFormattingModel(this.formattingSettings);
}
}
{
"dataRoles": [
{
"name": "Date",
"displayName": "field",
"kind": "Grouping"
}
],
"dataViewMappings": [
{
"conditions": [
{
"Date": {
"max": 1
}
}
],
"categorical": {
"categories": {
"for": {
"in": "Date"
}
}
}
}
],
"objects": {
"general": {
"displayName": "General",
"displayNameKey": "formattingGeneral",
"properties": {
"filter": {
"type": {
"filter": true
}
}
}
},
"labelFormatting": {
"displayName": "Slicer header",
"properties": {
"show": {
"type": {
"bool": true
}
},
"text": {
"displayName": "Text",
"type": {
"text": true
}
},
"fontFamily": {
"displayName": "Font",
"type": {
"formatting": {
"fontFamily": true
}
}
},
"fontSize": {
"displayName": "Font size",
"type": {
"numeric": true
}
},
"fontColor": {
"displayName": "Font color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
}
}
},
"inputFormatting": {
"displayName": "Values",
"properties": {
"fontFamily": {
"displayName": "Font",
"type": {
"formatting": {
"fontFamily": true
}
}
},
"fontSize": {
"displayName": "Font size",
"type": {
"numeric": true
}
},
"fontColor": {
"displayName": "Font color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
}
}
}
},
"sorting": {
"default": {}
},
"privileges": [],
"supportsEmptyDataView": true
}
Both dim start_date and dim end_date are individual tables without relationship to any table.