分层标识可筛选 Power BI 视觉对象中的 API

借助层次结构标识筛选器 API,使用 Matrix DataView 映射的视觉对象能够基于使用层次结构结构的数据点一次筛选多个字段上的数据。

此 API 在以下方案中非常有用:

  • 基于数据点筛选层次结构
  • 将语义模型与键上的组配合使用的自定义视觉对象

注意

层次结构标识筛选器 API 可从 API 版本 5.9.0 获取

筛选器接口如以下代码所示:

interface IHierarchyIdentityFilter<IdentityType> extends IFilter {
    target: IHierarchyIdentityFilterTarget;
    hierarchyData: IHierarchyIdentityFilterNode<IdentityType>[];
}
  • $schema:https://powerbi.com/product/schema#hierarchyIdentity(继承自 IFilter)

  • filterType:FilterType.HierarchyIdentity(继承自 IFilter)

  • 目标:查询中相关列的数组。 目前仅支持单个角色;因此,目标不是必需的,并且应为空。

  • hierarchyData:层次结构树中的已选项和未选项,其中每个 IHierarchyIdentityFilterNode<IdentityType> 都表示单个值选择。

type IHierarchyIdentityFilterTarget = IQueryNameTarget[]

interface IQueryNameTarget {
    queryName: string;
}
  • queryName:查询中源列的查询名称。 它来自 DataViewMetadataColumn
interface IHierarchyIdentityFilterNode<IdentityType> {
    identity: IdentityType;
    children?: IHierarchyIdentityFilterNode<IdentityType>[];
    operator: HierarchyFilterNodeOperators;
}
  • identity:DataView 中的节点标识。 IdentityType 应为 CustomVisualOpaqueIdentity

  • 子级:与当前所选内容相关的节点子级列表

  • 运算符:树中单个对象的运算符。 运算符可以是以下三个选项之一:

    type HierarchyFilterNodeOperators = "Selected" | "NotSelected" | "Inherited";
    
    • 已选择:值是显式选择的。

    • NotSelected:未显式选择值

    • 继承:值是根据层次结构中的父值选择的,或为默认值(如果它是根值)。

定义层次结构标识筛选器时,请记住以下规则:

  • 从 DataView 获取标识。
  • 每个标识路径都应是 DataView 中的有效路径。
  • 每个叶都应具有 Selected 或 NotSelected 运算符。
  • 要比较标识,请使用 ICustomVisualsOpaqueUtils.compareCustomVisualOpaqueIdentities 函数。
  • 标识可能会更改以下字段更改(例如添加或移除字段)。 Power BI 会将更新后的标识分配给现有的 filter.hierarchyData。

如何使用层次结构标识筛选器 API

以下代码是如何在自定义视觉对象中使用层次结构标识筛选器 API 的示例:

import { IHierarchyIdentityFilterTarget, IHierarchyIdentityFilterNode, HierarchyIdentityFilter } from "powerbi-models"

const target: IHierarchyIdentityFilterTarget = [];

const hierarchyData: IHierarchyIdentityFilterNode<CustomVisualOpaqueIdentity>[] = [
    {
        identity: {...},
        operator: "Selected",
        children: [
            {
                identity: {...},
                operator: "NotSelected"
            }
        ]
    },
    {
        identity: {...},
        operator: "Inherited",
        children: [
            {
                identity: {...},
                operator: "Selected"
            }
        ]
    }
];

const filter = new HierarchyIdentityFilter(target, hierarchyData).toJSON();

要应用筛选器,请使用 applyJsonFilter API 调用:

this.host.applyJsonFilter(filter, "general", "filter", action);

要还原活动 JSON 筛选器,请使用在“VisualUpdateOptions”中找到的 jsonFilters 属性:

export interface VisualUpdateOptions extends extensibility.VisualUpdateOptions {
   //...
   jsonFilters?: IFilter[];
}

只有分层相关字段支持使用 HierarchyIdnetity 筛选器。 默认情况下,Power BI 不会验证字段是否在层次结构方面相关。

要激活分层相关的验证,请将“areHierarchicallyRelated”属性添加到 capabilities.json 文件中的相关角色条件:

"dataViewMappings": [
    {
         "conditions": [
             {
                  "Rows": {
                      "min": 1,
                      "areHierarchicallyRelated": true <------ NEW ------>
                  },
                  "Value": {
                  "min": 0
                  }
            }
        ],
        ...
    }
]

如果满足以下条件,则字段在分层上相关:

  • 所包括的关系边缘不是多对多基数,也不是 ConceptualNavigationBehavior.Weak

  • 筛选器中的所有字段都存在于路径中。

  • 路径中的每个关系方向相同,或者是双向的。

  • 关系方向与一对多或双向基数匹配。

层次结构关系示例

例如,给定以下实体关系:

显示筛选器双向性质的关系图。

  • A 和 B 在层次结构方面相关:true
  • B 和 C 在层次结构方面相关:true
  • A、B、C 在层次结构方面相关:true
  • A、C、E 在层次结构方面相关:true(A -> E -> C)
  • A、B、E 在层次结构方面相关:true(B -> A -> E)
  • A、B、C、E 在层次结构方面相关:true(B -> A -> E -> C)
  • A、B、C、D 在层次结构方面相关:false(违反了规则 #3)
  • C、D 在层次结构方面相关:true
  • B、C、D 在层次结构方面相关:false(违反了规则 #3)
  • A、C、D、E 在层次结构方面相关:false(违反了规则 #3)

注意

  • 启用这些验证并且字段在层次结构方面不相关时,视觉对象不会呈现,并将显示一条错误消息:

    屏幕截图显示启用了验证的视觉对象无法加载,因为字段在层次结构方面不相关。错误消息显示“你正在使用的字段没有受支持的关系集”。

    屏幕截图显示了启用验证且字段在层次结构方面不相关时的错误消息。消息显示“无法显示此视觉对象”。

  • 如果禁用了这些验证,并且筛选器视觉对象应用的筛选器包含与在层次结构方面不相关字段相关的节点,则当度量值正在使用时,其他视觉对象可能无法正确呈现:

    屏幕截图显示禁用了验证的视觉对象无法加载,因为字段在层次结构方面不相关。错误消息显示“无法加载此视觉对象的数据”。

    屏幕截图显示了禁用验证且字段在层次结构方面不相关时的错误消息。消息显示“无法加载此视觉对象的数据”。

进行新的选择后更新层次结构数据树的代码示例

以下代码演示如何在进行新的选择后更新 hierarchyData 树:

type CompareIdentitiesFunc = (id1: CustomVisualOpaqueIdentity, id2: CustomVisualOpaqueIdentity) => boolean;
/**
* Updates the filter tree following a new node selection.
* Prunes irrelevant branches after node insertion/removal if necessary.
* @param path Identities path to the selected node.
* @param treeNodes Array of IHierarchyIdentityFilterNode representing a valid filter tree.
* @param compareIdentities Compare function for CustomVisualOpaqueIdentity to determine equality. Pass the ICustomVisualsOpaqueUtils.compareCustomVisualOpaqueIdentities function.
* @returns A valid filter tree after the update
*/

function updateFilterTreeOnNodeSelection(
   path: CustomVisualOpaqueIdentity[],
   treeNodes: IHierarchyIdentityFilterNode<CustomVisualOpaqueIdentity>[],
   compareIdentities: CompareIdentitiesFunc
): IHierarchyIdentityFilterNode<CustomVisualOpaqueIdentity>[] {
    if (!path) return treeNodes;
    const root: IHierarchyIdentityFilterNode<CustomVisualOpaqueIdentity> = {
        identity: null,
        children: treeNodes || [],
        operator: 'Inherited',
    };
    let currentNodesLevel = root.children;
    let isClosestSelectedParentSelected = root.operator === 'Selected';
    let parents: { node: IHierarchyIdentityFilterNode<CustomVisualOpaqueIdentity>, index: number }[] = [{ node: root, index: -1 }];
    let shouldFixTree = false;
    path.forEach((identity, level) => {
        const index = currentNodesLevel.findIndex((node) => compareIdentities(node.identity, identity));
        const isLastNodeInPath = level === path.length - 1
        if (index === -1) {
           const newNode: IHierarchyIdentityFilterNode<CustomVisualOpaqueIdentity> = {
               identity,
               children: [],
               operator: isLastNodeInPath ? (isClosestSelectedParentSelected ? 'NotSelected' : 'Selected') : 'Inherited',
           };
           currentNodesLevel.push(newNode);
           currentNodesLevel = newNode.children;
           if (newNode.operator !== 'Inherited') {
              isClosestSelectedParentSelected = newNode.operator === 'Selected';
           }
        } else {
            const currentNode = currentNodesLevel[index];
            if (isLastNodeInPath) {
               const partial = currentNode.children && currentNode.children.length;
               if (partial) {
                  /**
                   * The selected node has subtree.
                   * Therefore, selecting this node should lead to one of the following scenarios:
                   * 1. The node should have Selected operator and its subtree should be pruned.
                   * 2. The node and its subtree should be pruned form the tree and the tree should be fixed.
                   */
                   // The subtree should be always pruned.
                   currentNode.children = [];
                   if (currentNode.operator === 'NotSelected' || (currentNode.operator === 'Inherited' && isClosestSelectedParentSelected )) {
                      /**
                       * 1. The selected node has NotSelected operator.
                       * 2. The selected node has Inherited operator, and its parent has Slected operator.
                       * In both cases the node should be pruned from the tree and the tree shoud be fixed.
                       */
                      currentNode.operator = 'Inherited'; // to ensure it will be pruned
                      parents.push({ node: currentNode, index });
                      shouldFixTree = true;
                  } else {
                     /**
                      * 1. The selected node has Selected operator.
                      * 2. The selected node has Inherited operator, but its parent doesn't have Selected operator.
                      * In both cases the node should stay with Selected operator pruned from the tree and the tree should be fixed.
                      * Note that, node with Selected oprator and parent with Selector operator is not valid state.
                      */
                      currentNode.operator = 'Selected';
                  }
              } else {
                  // Leaf node. The node should be pruned from the tree and the tree should be fixed.
                  currentNode.operator = 'Inherited'; // to ensure it will be pruned
                  parents.push({ node: currentNode, index });
                  shouldFixTree = true;
                 }
             } else {
                 // If it's not the last noded in path we just continue traversing the tree
                 currentNode.children = currentNode.children || [];
                 currentNodesLevel = currentNode.children
                 if (currentNode.operator !== 'Inherited') {
                     isClosestSelectedParentSelected = currentNode.operator === 'Selected';
                     // We only care about the closet parent with Selected/NotSelected operator and its children
                     parents = [];
                  }
                  parents.push({ node: currentNode, index });
                }
           }
    });
    // Prune brnaches with Inherited leaf
    if (shouldFixTree) {
       for (let i = parents.length - 1; i >= 1; i--) {
           // Normalize to empty array
           parents[i].node.children = parents[i].node.children || [];
           if (!parents[i].node.children.length && (parents[i].node.operator === 'Inherited')) {
              // Remove the node from its parent children array
              removeElement(parents[i - 1].node.children, parents[i].index);
           } else {
               // Node has children or Selected/NotSelected operator
               break;
         }
      }
   }
   return root.children;
}
/**
* Removes an element from the array without preserving order.
* @param arr - The array from which to remove the element.
* @param index - The index of the element to be removed.
*/
function removeElement(arr: any[], index: number): void {
    if (!arr || !arr.length || index < 0 || index >= arr.length) return;
    arr[index] = arr[arr.length - 1];
    arr.pop();
}

注意事项和限制

  • 仅矩阵 dataView 映射支持此筛选器。

  • 视觉对象应仅包含一个分组数据角色

  • 使用层次结构标识筛选器类型的视觉对象应仅应用此类型的单个筛选器。