閱讀英文

共用方式為


Power BI 視覺效果中的階層式身分識別篩選 API

階層身分識別篩選 API 可讓使用矩陣 DataView 對應的視覺效果,根據使用階層結構的資料點,一次篩選多個欄位上的資料。

此 API 在下列案例中很有用:

  • 根據資料點篩選階層
  • 使用語意模型搭配索引鍵群組的自訂視覺效果

注意

階層身分識別篩選 API 可從 API 版本 5.9.0 取得

篩選介面如下列程式碼所示:

interface IHierarchyIdentityFilter<IdentityType> extends IFilter {
    target: IHierarchyIdentityFilterTarget;
    hierarchyData: IHierarchyIdentityFilterNode<IdentityType>[];
}
  • $schemahttps://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;
}
  • 身分識別:DataView 中的節點身分識別。 IdentityType 應該是 CustomVisualOpaqueIdentity

  • 子系:與目前選取項目相關的節點子系清單

  • 運算子:樹狀結構中單一物件的運算子。 運算子可以是下列三個選項之一:

    type HierarchyFilterNodeOperators = "Selected" | "NotSelected" | "Inherited";
    
    • Selected:已明確選取值。

    • NotSelected:未明確選取值。

    • Inherited:根據階層中的父代值選取值,或者若為根值則使用預設值。

定義階層身分識別篩選時,請記住下列規則:

  • DataView 取得身分識別。
  • 每個 identity 路徑都應該是 DataView 中的有效路徑。
  • 每個分葉都應該有一個 SelectedNotSelected 運算子。
  • 若要比較身分識別,請使用 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 對應支援此篩選。

  • 視覺效果應該只包含 群組 資料角色

  • 使用階層身分識別篩選類型的視覺效果應該只套用此類型的單一篩選。