다음을 통해 공유


Power BI 시각적 개체의 계층 구조 ID 필터 API

계층 구조 ID 필터 API를 사용하면 행렬 DataView 매핑을 사용하여 계층 구조를 사용하는 데이터 포인트를 기반으로 한 번에 여러 필드의 데이터를 필터링하는 시각적 개체를 사용할 수 있습니다.

이 API는 다음과 같은 시나리오에 유용합니다.

  • 데이터 포인트를 기반으로 계층 구조 필터링
  • 키 그룹이 포함된 의미 체계 모델을 사용하는 사용자 지정 시각적 개체

참고 항목

계층 구조 ID 필터 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에서 상속됨)

  • target: 쿼리의 관련 열 배열입니다. 현재 단일 역할만 지원됩니다. 따라서 대상은 필요하지 않으며 비어 있어야 합니다.

  • hierarchyData: 계층 구조 트리에서 선택한 항목과 선택되지 않은 항목으로, 각 IHierarchyIdentityFilterNode<IdentityType>은 단일 값 선택을 나타냅니다.

type IHierarchyIdentityFilterTarget = IQueryNameTarget[]

interface IQueryNameTarget {
    queryName: string;
}
  • queryName: 쿼리에 있는 원본 열의 쿼리 이름입니다. DataViewMetadataColumn에서 가져옵니다.
interface IHierarchyIdentityFilterNode<IdentityType> {
    identity: IdentityType;
    children?: IHierarchyIdentityFilterNode<IdentityType>[];
    operator: HierarchyFilterNodeOperators;
}
  • identity: DataView의 노드 ID입니다. IdentityTypeCustomVisualOpaqueIdentity여야 합니다.

  • children: 현재 선택 항목과 관련된 노드 자식 목록입니다.

  • operator: 트리의 단일 개체에 대한 연산자입니다. 연산자는 다음 세 가지 옵션 중 하나일 수 있습니다.

    type HierarchyFilterNodeOperators = "Selected" | "NotSelected" | "Inherited";
    
    • Selected: 값이 명시적으로 선택됩니다.

    • NotSelected: 값이 명시적으로 선택되지 않습니다.

    • Inherited: 값 선택이 계층 구조의 부모 값을 따르거나 루트 값인 경우 기본값입니다.

계층 구조 ID 필터를 정의할 때 다음 규칙을 염두에 두세요.

  • DataView에서 ID를 가져옵니다.
  • ID 경로는 DataView에서 유효한 경로여야 합니다.
  • 모든 리프에는 Selected 또는 NotSelected 연산자가 있어야 합니다.
  • ID를 비교하려면 ICustomVisualsOpaqueUtils.compareCustomVisualOpaqueIdentities 함수를 사용합니다.
  • ID는 다음 필드 변경 사항을 바꿀 수 있습니다(예: 필드 추가 또는 제거). Power BI는 업데이트된 ID를 기존 filter.hierarchyData에 할당합니다.

계층 구조 ID 필터 API를 사용하는 방법

다음 코드는 사용자 지정 시각적 개체에서 계층 구조 ID 필터 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는 필드가 계층적으로 관련되어 있는지 검증하지 않습니다.

계층적 관련 검증을 활성화하려면 capabilities.json 파일의 관련 역할 조건에 'areHierarchicallyRelated' 속성을 추가합니다.

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

다음 조건이 충족되면 필드는 계층적으로 관련됩니다.

  • 포함된 관계 에지가 다대다 카디널리티나 ConceptualNavigationBehavior.Weak가 아닙니다.

  • 필터의 모든 필드가 경로에 있습니다.

  • 경로의 모든 관계가 동일한 방향 또는 양방향을 갖습니다.

  • 관계 방향이 일대다 또는 양방향의 카디널리티와 일치합니다.

계층 구조 관계 예

예를 들어 다음과 같은 엔터티 관계가 있다고 가정합니다.

필터의 양방향 특성을 보여 주는 다이어그램.

  • A, B가 계층적으로 관련되어 있음: 참
  • B, C가 계층적으로 관련되어 있음: 참
  • A, B, C가 계층적으로 관련되어 있음: 참
  • A, C, E가 계층적으로 관련되어 있음: 참(A --> E --> C)
  • A, B, E가 계층적으로 관련되어 있음: 참(B --> A --> E)
  • A, B, C, E가 계층적으로 관련되어 있음: 참(B --> A --> E --> C)
  • A, B, C, D가 계층적으로 관련되어 있음: 거짓(규칙 #3 위반)
  • C, D가 계층적으로 관련되어 있음: 참
  • B, C, D가 계층적으로 관련되어 있음: 거짓(규칙 #3 위반)
  • A, C, D, E가 계층적으로 관련되어 있음: 거짓(규칙 #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 매핑에 대해서만 지원됩니다.

  • 시각적 개체에는 하나의 그룹화 데이터 역할만 포함되어야 합니다.

  • 계층 구조 ID 필터 유형을 사용하는 시각적 개체는 이 유형의 단일 필터만 적용해야 합니다.