package com.linkedin.thirdeye.dashboard.views.diffsummary; import com.linkedin.thirdeye.client.diffsummary.Cube; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.linkedin.thirdeye.client.diffsummary.CostFunction; import com.linkedin.thirdeye.client.diffsummary.Dimensions; import com.linkedin.thirdeye.client.diffsummary.HierarchyNode; public class SummaryResponseTree { @JsonProperty("dimensions") List<String> dimensions = new ArrayList<>(); List<HierarchyNode> hierarchicalNodes = new ArrayList<>(); public static List<HierarchyNode> sortResponseTree(List<HierarchyNode> nodes, int levelCount) { SummaryResponseTree responseTree = new SummaryResponseTree(); // Build the header Dimensions dimensions = nodes.get(0).getDimensions(); double totalValue = nodes.get(0).getOriginalCurrentValue() + nodes.get(0).getOriginalBaselineValue(); for (int i = 0; i < levelCount; ++i) { responseTree.dimensions.add(dimensions.get(i)); } List<SummaryResponseTreeNode> treeNodes = new ArrayList<>(); // Build the response tree Collections.sort(nodes, Collections.reverseOrder(Summary.NODE_COMPARATOR)); // pre-order traversal for (HierarchyNode node : nodes) { SummaryResponseTreeNode treeNode = new SummaryResponseTreeNode(); treeNode.hierarchyNode = node; treeNode.level = node.getLevel(); treeNodes.add(treeNode); } // Connecting child and parent response tree node. Note: response tree is not a perfect tree like the tree // of HierarchyNodes, because in response tree a node's direct parent may be missing. // In that case, we have to bootstrap the search until a higher level parent, which also exists in the response // tree, is found. // Pre-condition: treeNodes are sorted in the pre-order fashion when projecting the nodes back to the tree of // HierarchyNode. SummaryResponseTreeNode preTreeNode = null; for (SummaryResponseTreeNode treeNode : treeNodes) { if (preTreeNode != null) { SummaryResponseTreeNode parent = preTreeNode.getCommonParent(treeNode.getDimensionValues()); treeNode.parent = parent; parent.children.add(treeNode); } preTreeNode = treeNode; } // Sort the children of each node by their cost sortChildNodes(treeNodes.get(0), totalValue); // Put the nodes to a flattened array insertChildNodes(treeNodes.get(0), responseTree.hierarchicalNodes); return responseTree.hierarchicalNodes; } private static void insertChildNodes(SummaryResponseTreeNode node, List<HierarchyNode> hierarchicalNodes) { if (node.hierarchyNode != null) hierarchicalNodes.add(node.hierarchyNode); for (SummaryResponseTreeNode child : node.children) { insertChildNodes(child, hierarchicalNodes); } } /** * A recursive function to sort response tree. */ private static void sortChildNodes(SummaryResponseTreeNode node, double totalValue) { if (node.children.size() == 0) return; for (SummaryResponseTreeNode child : node.children) { sortChildNodes(child, totalValue); } double ratio = node.currentRatio(); for (SummaryResponseTreeNode child : node.children) { computeCost(child, ratio, totalValue); } Collections.sort(node.children, Collections.reverseOrder(new SummaryResponseTreeNodeCostComparator())); } private static void computeCost(SummaryResponseTreeNode node, double targetRatio, double totalValue) { if (node.hierarchyNode != null) { node.cost = CostFunction .errWithPercentageRemoval(node.getBaselineValue(), node.getCurrentValue(), targetRatio, Cube.PERCENTAGE_CONTRIBUTION_THRESHOLD, totalValue); } for (SummaryResponseTreeNode child : node.children) { computeCost(child, targetRatio, totalValue); node.cost += child.cost; } } public static class SummaryResponseTreeNodeCostComparator implements Comparator<SummaryResponseTreeNode> { @Override public int compare(SummaryResponseTreeNode o1, SummaryResponseTreeNode o2) { return Double.compare(o1.cost, o2.cost); } } public static class SummaryResponseTreeNode { HierarchyNode hierarchyNode; // If it is null, this node is a dummy node. double cost; int level; @JsonIgnore SummaryResponseTreeNode parent; List<SummaryResponseTreeNode> children = new ArrayList<>(); public List<String> getDimensionValues() { return hierarchyNode.getDimensionValues().values(); } public void setLevel(int level) { this.level = level; } public int getLevel() { return level; } public double getBaselineValue() { return hierarchyNode.getBaselineValue(); } public double getCurrentValue() { return hierarchyNode.getCurrentValue(); } public double currentRatio() { if (hierarchyNode != null) { return hierarchyNode.currentRatio(); } else { SummaryResponseTreeNode parent = this; do { if (parent.hierarchyNode == null) { parent = parent.parent; } else { break; } } while (true); return parent.currentRatio(); } } @JsonIgnore /** * Return the common parent node according to the given dimension values. There always exists one common * parent: the root of the tree. */ public SummaryResponseTreeNode getCommonParent(List<String> otherDimensionValues) { // Calculate the level of the common parent int targetLevel = 0; Iterator<String> otherIte = otherDimensionValues.iterator(); for (targetLevel = 0; targetLevel < getLevel(); ++targetLevel) { if (otherIte.hasNext()) { String otherDimensionValue = otherIte.next(); if ( !getDimensionValues().get(targetLevel).equals(otherDimensionValue) ) { break; } // else continue } else { break; } } // Return the common parent SummaryResponseTreeNode node = this; SummaryResponseTreeNode preNode = this; while (true) { if (node.getLevel() == targetLevel) { return node; } if (node.getLevel() < targetLevel) { // current node becomes the missing common parent and move itself to a new node SummaryResponseTreeNode newChildNode = new SummaryResponseTreeNode(); swapContent(preNode, newChildNode); newChildNode.parent = preNode; preNode.children.add(newChildNode); preNode.level = targetLevel; return preNode; } else { preNode = node; node = node.parent; } } } private void swapContent(SummaryResponseTreeNode A, SummaryResponseTreeNode B) { HierarchyNode tmpNode = A.hierarchyNode; A.hierarchyNode = B.hierarchyNode; B.hierarchyNode = tmpNode; List<SummaryResponseTreeNode> tmpChildren = A.children; A.children = B.children; B.children = tmpChildren; int tmpLevel = A.level; A.level = B.level; B.level = tmpLevel; } } }