/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.graphs;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.collections15.Factory;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.operator.learner.tree.Edge;
import com.rapidminer.operator.learner.tree.NominalSplitCondition;
import com.rapidminer.operator.learner.tree.SplitCondition;
import com.rapidminer.operator.learner.tree.Tree;
import com.rapidminer.operator.learner.tree.TreeModel;
import com.rapidminer.tools.Tools;
import edu.uci.ics.jung.graph.DelegateForest;
import edu.uci.ics.jung.graph.DirectedOrderedSparseMultigraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.visualization.renderers.Renderer.EdgeLabel;
import edu.uci.ics.jung.visualization.renderers.Renderer.Vertex;
import edu.uci.ics.jung.visualization.renderers.Renderer.VertexLabel;
/**
* Creates a graph model for a learned tree model.
*
* @author Ingo Mierswa
*/
public class TreeModelGraphCreator extends GraphCreatorAdaptor {
private Factory<String> edgeFactory = new Factory<String>() {
int i = 0;
@Override
public String create() {
return "E" + i++;
}
};
private Factory<String> vertexFactory = new Factory<String>() {
int i = 0;
@Override
public String create() {
return "V" + i++;
}
};
private TreeModel model;
private Map<String, Tree> vertexMap = new HashMap<>();
private Map<String, SplitCondition> edgeMap = new HashMap<>();
private Map<String, Double> edgeStrengthMap = new HashMap<>();
private Map<String, List<String>> pathToRootMap = new HashMap<>();
public TreeModelGraphCreator(TreeModel model) {
this.model = model;
}
public Tree getTree(String id) {
return vertexMap.get(id);
}
@Override
public String getVertexName(String object) {
Tree node = vertexMap.get(object);
String name = "";
if (node != null) {
if (node.isLeaf()) {
name = node.getLabel();
} else {
Iterator<Edge> e = node.childIterator();
while (e.hasNext()) {
SplitCondition condition = e.next().getCondition();
name = condition.getAttributeName();
break;
}
}
}
return name;
}
@Override
public String getVertexToolTip(String object) {
Tree tree = vertexMap.get(object);
if (tree != null) {
return createTooltip(tree);
} else {
return null;
}
}
@Override
public String getEdgeName(String object) {
SplitCondition condition = edgeMap.get(object);
if (condition != null) {
if (condition instanceof NominalSplitCondition) {
return condition.getValueString();
} else {
return condition.getRelation() + " " + condition.getValueString();
}
} else {
return null;
}
}
@Override
public boolean isLeaf(String object) {
Tree tree = vertexMap.get(object);
if (tree != null) {
return tree.isLeaf();
} else {
return false;
}
}
@Override
public boolean isEdgeOnSelectedPath(Set<String> selectedVertexes, String id) {
// both edges and vertexes are in the same map
return isVertexOnSelectedPath(selectedVertexes, id);
}
@Override
public boolean isVertexOnSelectedPath(Set<String> selectedVertexes, String id) {
boolean onPath = false;
for (String selectedVertex : selectedVertexes) {
List<String> pathElements = pathToRootMap.get(selectedVertex);
if (pathElements != null) {
onPath = pathElements.contains(id);
}
if (onPath) {
return true;
}
}
return false;
}
@Override
public Graph<String, String> createGraph() {
DirectedOrderedSparseMultigraph<String, String> treeGraph = new DirectedOrderedSparseMultigraph<String, String>();
Tree root = this.model.getRoot();
treeGraph.addVertex("Root");
vertexMap.put("Root", root);
addTree(treeGraph, root, "Root", new ArrayList<>());
return new DelegateForest<String, String>(treeGraph);
}
private void addTree(DirectedOrderedSparseMultigraph<String, String> treeGraph, Tree node, String parentName,
List<String> currentParentList) {
Iterator<Edge> e = node.childIterator();
double edgeWeightSum = model.getRoot().getSubtreeFrequencySum();
e = node.childIterator();
while (e.hasNext()) {
Edge edge = e.next();
Tree child = edge.getChild();
SplitCondition condition = edge.getCondition();
String childName = vertexFactory.create();
String edgeName = edgeFactory.create();
currentParentList.add(parentName);
currentParentList.add(edgeName);
pathToRootMap.put(edgeName, currentParentList);
pathToRootMap.put(childName, currentParentList);
vertexMap.put(childName, child);
edgeMap.put(edgeName, condition);
edgeStrengthMap.put(edgeName, child.getSubtreeFrequencySum() / edgeWeightSum);
treeGraph.addEdge(edgeName, parentName, childName);
addTree(treeGraph, child, childName, new ArrayList<>(currentParentList));
// siblings would use the same list with wrong edges if we did not copy
currentParentList = new ArrayList<String>(currentParentList);
currentParentList.remove(edgeName);
}
}
/**
* Returns the model.
*/
public TreeModel getModel() {
return model;
}
@Override
public Vertex<String, String> getVertexRenderer() {
int maxSize = -1;
Tree root = model.getRoot();
maxSize = getMaximumLeafSize(root, maxSize);
return new TreeModelNodeRenderer<String, String>(this, maxSize);
}
private int getMaximumLeafSize(Tree tree, int max) {
if (tree.isLeaf()) {
return Math.max(max, tree.getFrequencySum());
} else {
Iterator<Edge> e = tree.childIterator();
int maximum = max;
while (e.hasNext()) {
Edge edge = e.next();
Tree child = edge.getChild();
maximum = Math.max(maximum, getMaximumLeafSize(child, maximum));
}
return maximum;
}
}
@Override
public EdgeLabel<String, String> getEdgeLabelRenderer() {
return new TreeModelEdgeLabelRenderer<String, String>();
}
@Override
public VertexLabel<String, String> getVertexLabelRenderer() {
return new TreeModelNodeLabelRenderer<String, String>(this);
}
@Override
public boolean isEdgeLabelDecorating() {
return true;
}
@Override
public int getMinLeafHeight() {
return 45;
}
@Override
public int getMinLeafWidth() {
return 70;
}
@Override
public boolean isBold(String id) {
return isLeaf(id);
}
@Override
public boolean isRotatingEdgeLabels() {
return false;
}
@Override
public double getEdgeStrength(String id) {
Double value = edgeStrengthMap.get(id);
if (value == null) {
return 1.0d;
} else {
if (Double.isNaN(value)) {
return 1.0d;
} else {
return value;
}
}
}
@Override
public Object getObject(String id) {
return vertexMap.get(id);
}
/** Returns 0 (for other values the edge label painting will not work). */
@Override
public int getLabelOffset() {
return 0;
}
/**
* Create the tooltip for a tree node.
*
* @param tree
* the tree for which to create the tooltip
* @return the HTML-formatted tooltip string
*/
private String createTooltip(Tree tree) {
StringBuilder sb = new StringBuilder();
if (tree.isLeaf()) {
String labelString = tree.getLabel();
if (labelString != null) {
sb.append(
"<html><div style=\"font-size: 10px; font-family: 'Open Sans'\"><p style=\"font-size: 110%; text-align: center; font-family: 'Open Sans Semibold'\"><b>"
+ labelString + "</b><hr NOSHADE style=\"color: '#000000'; width: 95%; \"/></p>");
sb.append(SwingTools.transformToolTipText(formatCounterMap(tree.getCounterMap()), false, 200, false, false)
+ "<br/>");
sb.append("Number of items: " + tree.getFrequencySum() + "<br/>");
sb.append("Ratio of total: "
+ Tools.formatPercent((double) tree.getFrequencySum() / model.getRoot().getSubtreeFrequencySum()));
sb.append("</div></html>");
}
} else {
sb.append("<html><div style=\"font-size: 10px; font-family: 'Open Sans'\">");
sb.append("<p>" + tree.getSubtreeFrequencySum() + " items in subtree</p><br/>");
sb.append("Ratio of total: " + Tools
.formatPercent((double) tree.getSubtreeFrequencySum() / model.getRoot().getSubtreeFrequencySum()));
sb.append("</div></html>");
}
return sb.toString();
}
/**
* Displays the tree counterMap contents nicely formatted for humans.
*
* @param counterMap
* the counter map of the tree to format
* @return the formatted string
*/
private static String formatCounterMap(Map<String, Integer> counterMap) {
StringBuilder sb = new StringBuilder();
sb.append("Distribution: ");
int size = counterMap.size();
for (Entry<String, Integer> item : counterMap.entrySet()) {
sb.append(item.getValue());
sb.append(' ');
sb.append("<i>");
sb.append(item.getKey());
sb.append("</i>");
if (--size > 0) {
sb.append(',').append(' ');
}
}
return sb.toString();
}
}