/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
* PrefuseTree.java
* Copyright (C) 2009 University of Waikato, Hamilton, New Zealand
* Copyright (C) Jeffrey Heer (original prefuse demo)
*/
package wekaexamples.gui.visualize.plugins;
import prefuse.Constants;
import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.Action;
import prefuse.action.ActionList;
import prefuse.action.ItemAction;
import prefuse.action.RepaintAction;
import prefuse.action.animate.ColorAnimator;
import prefuse.action.animate.LocationAnimator;
import prefuse.action.animate.QualityControlAnimator;
import prefuse.action.animate.VisibilityAnimator;
import prefuse.action.assignment.ColorAction;
import prefuse.action.assignment.FontAction;
import prefuse.action.filter.FisheyeTreeFilter;
import prefuse.action.layout.CollapsedSubtreeLayout;
import prefuse.action.layout.graph.NodeLinkTreeLayout;
import prefuse.activity.SlowInSlowOutPacer;
import prefuse.controls.FocusControl;
import prefuse.controls.PanControl;
import prefuse.controls.WheelZoomControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.Tree;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.io.TreeMLReader;
import prefuse.data.search.PrefixSearchTupleSet;
import prefuse.data.tuple.TupleSet;
import prefuse.render.AbstractShapeRenderer;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.EdgeRenderer;
import prefuse.render.LabelRenderer;
import prefuse.util.ColorLib;
import prefuse.util.FontLib;
import prefuse.visual.VisualItem;
import prefuse.visual.expression.InGroupPredicate;
import prefuse.visual.sort.TreeDepthItemSorter;
import weka.gui.treevisualizer.Edge;
import weka.gui.treevisualizer.Node;
import weka.gui.treevisualizer.TreeBuild;
import weka.gui.visualize.plugins.TreeVisualizePlugin;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
/**
* Displays a tree in <a href="http://www.graphviz.org/doc/info/lang.html" target="_blank">GraphViz's dotty</a>
* format as <a href="http://prefuse.org/" target="_blank">Prefuse</a> tree.
* <p/>
* See also the <a href="http://www.nomencurator.org/InfoVis2003/download/treeml.dtd" target="_blank">treeml.dtd</a>.
* <p/>
* Based on the <code>prefuse.demos.TreeView</code> demo class.
*
* @author <a href="http://jheer.org">jeffrey heer</a> (original prefuse demo)
* @author fracpete (fracpete at waikato dot ac dot nz)
* @version $Revision$
* @see prefuse.demos.TreeView
*/
public class PrefuseTree
implements Serializable, TreeVisualizePlugin {
/** for serialization. */
private static final long serialVersionUID = 7485599985684890717L;
/**
* Turns the <a href="http://www.graphviz.org/doc/info/lang.html" target="_blank">GraphViz dotty</a>
* format into Prefuse's tree XML format (according to the tree.dtd).
*
* @author fracpete (fracpete at waikato dot ac dot nz)
* @version $Revision$
*/
public static class DottyToTree {
/**
* Replaces certain characters with their character entities.
*
* @param s the string to process
* @return the processed string
*/
protected String sanitize(String s) {
String result;
result = s;
result = result.replaceAll("&", "&")
.replaceAll("\"", """)
.replaceAll("'", "'")
.replaceAll("<", "<")
.replaceAll(">", ">");
// in addition, replace some other entities as well
result = result.replaceAll("\n", "
")
.replaceAll("\r", "
")
.replaceAll("\t", " ");
return result;
}
/**
* Writes the header of the GraphML file.
*
* @param writer the writer to use
* @throws Exception if an error occurs
*/
protected void writeHeader(BufferedWriter writer) throws Exception {
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); writer.newLine();
writer.newLine();
writer.write("<!-- This file was generated by Weka (http://www.cs.waikato.ac.nz/ml/weka/). -->"); writer.newLine();
writer.newLine();
writer.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.nomencurator.org/InfoVis2003/download/treeml.dtd\">");
writer.newLine();
writer.write("<tree>"); writer.newLine();
writer.write("<declarations>"); writer.newLine();
writer.write("<attributeDecl name=\"name\" type=\"String\"/>"); writer.newLine();
writer.write("</declarations>"); writer.newLine();
}
/**
* Writes the node as GraphML.
*
* @param writer the writer to use
* @param node the node to write as GraphML
* @throws Exception if an error occurs
*/
protected void writeNode(BufferedWriter writer, Node node) throws Exception {
int i;
String tag;
boolean leaf;
// leaf?
leaf = (node.getChild(0) == null);
if (leaf)
tag = "leaf";
else
tag = "branch";
// the node itself
writer.write("<" + tag + ">"); writer.newLine();
writer.write("<attribute name=\"name\" value=\"" + sanitize(node.getLabel()) + "\"/>"); writer.newLine();
// the node's children, if any
if (!leaf) {
for (i = 0; (node.getChild(i) != null); i++)
writeEdge(writer, node.getChild(i));
}
writer.write("</" + tag + ">"); writer.newLine();
}
/**
* Writes the edge as GraphML. Since prefuse doesn't seem to offer edge
* labels, the edges get inserted as nodes as well.
*
* @param writer the writer to use
* @param edge the edge to write
* @throws Exception if an error occurs
*/
protected void writeEdge(BufferedWriter writer, Edge edge) throws Exception {
if (edge.getLabel().length() > 0) {
writer.write("<branch>"); writer.newLine();
writer.write("<attribute name=\"name\" value=\"" + sanitize(edge.getLabel()) + "\"/>"); writer.newLine();
writeNode(writer, edge.getTarget());
writer.write("</branch>"); writer.newLine();
}
else {
writeNode(writer, edge.getTarget());
}
}
/**
* Writes the footer of the GraphML file.
*
* @param writer the writer to use
* @throws Exception if an error occurs
*/
protected void writeFooter(BufferedWriter writer) throws Exception {
writer.write("</tree>"); writer.newLine();
}
/**
* Parses the incoming data and writes the generated output.
*
* @param dotty the graph in dotty format
* @return the TreeML data
* @throws Exception if parsing or writing fails
*/
public String convert(String dotty) throws Exception {
Node root;
TreeBuild tree;
StringWriter output;
BufferedWriter writer;
// parse dotty format
tree = new TreeBuild();
root = tree.create(new StringReader(dotty));
// generate GraphML output
output = new StringWriter();
writer = new BufferedWriter(output);
writeHeader(writer);
writeNode(writer, root);
writeFooter(writer);
writer.flush();
return output.toString();
}
}
/**
* Displays a tree.
* <p/>
* Based on the <code>prefuse.demos.TreeView</code> demo class.
*
* @author fracpete (fracpete at waikato dot ac dot nz)
* @version $Revision$
* @see prefuse.demos.TreeView
*/
public static class TreePanel
extends Display {
/** for serialization. */
private static final long serialVersionUID = 8262123080545898882L;
public class OrientAction extends AbstractAction {
private int orientation;
public OrientAction(int orientation) {
this.orientation = orientation;
}
public void actionPerformed(ActionEvent evt) {
setOrientation(orientation);
getVisualization().cancel("orient");
getVisualization().run("treeLayout");
getVisualization().run("orient");
}
}
public class AutoPanAction extends Action {
private Point2D m_start = new Point2D.Double();
private Point2D m_end = new Point2D.Double();
private Point2D m_cur = new Point2D.Double();
private int m_bias = 150;
public void run(double frac) {
TupleSet ts = m_vis.getFocusGroup(Visualization.FOCUS_ITEMS);
if ( ts.getTupleCount() == 0 )
return;
if ( frac == 0.0 ) {
int xbias=0, ybias=0;
switch ( m_orientation ) {
case Constants.ORIENT_LEFT_RIGHT:
xbias = m_bias;
break;
case Constants.ORIENT_RIGHT_LEFT:
xbias = -m_bias;
break;
case Constants.ORIENT_TOP_BOTTOM:
ybias = m_bias;
break;
case Constants.ORIENT_BOTTOM_TOP:
ybias = -m_bias;
break;
}
VisualItem vi = (VisualItem)ts.tuples().next();
m_cur.setLocation(getWidth()/2, getHeight()/2);
getAbsoluteCoordinate(m_cur, m_start);
m_end.setLocation(vi.getX()+xbias, vi.getY()+ybias);
} else {
m_cur.setLocation(m_start.getX() + frac*(m_end.getX()-m_start.getX()),
m_start.getY() + frac*(m_end.getY()-m_start.getY()));
panToAbs(m_cur);
}
}
}
public static class NodeColorAction extends ColorAction {
public NodeColorAction(String group) {
super(group, VisualItem.FILLCOLOR);
}
public int getColor(VisualItem item) {
if ( m_vis.isInGroup(item, Visualization.SEARCH_ITEMS) )
return ColorLib.rgb(255,190,190);
else if ( m_vis.isInGroup(item, Visualization.FOCUS_ITEMS) )
return ColorLib.rgb(198,229,229);
else if ( item.getDOI() > -1 )
return ColorLib.rgb(164,193,193);
else
return ColorLib.rgba(255,255,255,0);
}
} // end of inner class TreeMapColorAction
private static final String tree = "tree";
private static final String treeNodes = "tree.nodes";
private static final String treeEdges = "tree.edges";
private LabelRenderer m_nodeRenderer;
private EdgeRenderer m_edgeRenderer;
private String m_label = "name";
private int m_orientation = Constants.ORIENT_LEFT_RIGHT;
/**
* Initializes the panel.
*
* @param t the tree to visualize
*/
public TreePanel(Tree t) {
super(new Visualization());
m_vis.add(tree, t);
m_nodeRenderer = new LabelRenderer(m_label);
m_nodeRenderer.setRenderType(AbstractShapeRenderer.RENDER_TYPE_FILL);
m_nodeRenderer.setHorizontalAlignment(Constants.LEFT);
m_nodeRenderer.setRoundedCorner(8,8);
m_edgeRenderer = new EdgeRenderer(Constants.EDGE_TYPE_CURVE);
DefaultRendererFactory rf = new DefaultRendererFactory(m_nodeRenderer);
rf.add(new InGroupPredicate(treeEdges), m_edgeRenderer);
m_vis.setRendererFactory(rf);
// colors
ItemAction nodeColor = new NodeColorAction(treeNodes);
ItemAction textColor = new ColorAction(treeNodes,
VisualItem.TEXTCOLOR, ColorLib.rgb(0,0,0));
m_vis.putAction("textColor", textColor);
ItemAction edgeColor = new ColorAction(treeEdges,
VisualItem.STROKECOLOR, ColorLib.rgb(200,200,200));
// quick repaint
ActionList repaint = new ActionList();
repaint.add(nodeColor);
repaint.add(new RepaintAction());
m_vis.putAction("repaint", repaint);
// full paint
ActionList fullPaint = new ActionList();
fullPaint.add(nodeColor);
m_vis.putAction("fullPaint", fullPaint);
// animate paint change
ActionList animatePaint = new ActionList(400);
animatePaint.add(new ColorAnimator(treeNodes));
animatePaint.add(new RepaintAction());
m_vis.putAction("animatePaint", animatePaint);
// create the tree layout action
NodeLinkTreeLayout treeLayout = new NodeLinkTreeLayout(tree,
m_orientation, 50, 0, 8);
treeLayout.setLayoutAnchor(new Point2D.Double(25,300));
m_vis.putAction("treeLayout", treeLayout);
CollapsedSubtreeLayout subLayout =
new CollapsedSubtreeLayout(tree, m_orientation);
m_vis.putAction("subLayout", subLayout);
AutoPanAction autoPan = new AutoPanAction();
// create the filtering and layout
ActionList filter = new ActionList();
filter.add(new FisheyeTreeFilter(tree, 2));
filter.add(new FontAction(treeNodes, FontLib.getFont("Tahoma", 16)));
filter.add(treeLayout);
filter.add(subLayout);
filter.add(textColor);
filter.add(nodeColor);
filter.add(edgeColor);
m_vis.putAction("filter", filter);
// animated transition
ActionList animate = new ActionList(1000);
animate.setPacingFunction(new SlowInSlowOutPacer());
animate.add(autoPan);
animate.add(new QualityControlAnimator());
animate.add(new VisibilityAnimator(tree));
animate.add(new LocationAnimator(treeNodes));
animate.add(new ColorAnimator(treeNodes));
animate.add(new RepaintAction());
m_vis.putAction("animate", animate);
m_vis.alwaysRunAfter("filter", "animate");
// create animator for orientation changes
ActionList orient = new ActionList(2000);
orient.setPacingFunction(new SlowInSlowOutPacer());
orient.add(autoPan);
orient.add(new QualityControlAnimator());
orient.add(new LocationAnimator(treeNodes));
orient.add(new RepaintAction());
m_vis.putAction("orient", orient);
// ------------------------------------------------
// initialize the display
setSize(700,600);
setItemSorter(new TreeDepthItemSorter());
addControlListener(new ZoomToFitControl());
addControlListener(new ZoomControl());
addControlListener(new WheelZoomControl());
addControlListener(new PanControl());
addControlListener(new FocusControl(1, "filter"));
registerKeyboardAction(
new OrientAction(Constants.ORIENT_LEFT_RIGHT),
"left-to-right", KeyStroke.getKeyStroke("ctrl 1"), WHEN_FOCUSED);
registerKeyboardAction(
new OrientAction(Constants.ORIENT_TOP_BOTTOM),
"top-to-bottom", KeyStroke.getKeyStroke("ctrl 2"), WHEN_FOCUSED);
registerKeyboardAction(
new OrientAction(Constants.ORIENT_RIGHT_LEFT),
"right-to-left", KeyStroke.getKeyStroke("ctrl 3"), WHEN_FOCUSED);
registerKeyboardAction(
new OrientAction(Constants.ORIENT_BOTTOM_TOP),
"bottom-to-top", KeyStroke.getKeyStroke("ctrl 4"), WHEN_FOCUSED);
// ------------------------------------------------
// filter graph and perform layout
setOrientation(m_orientation);
m_vis.run("filter");
TupleSet search = new PrefixSearchTupleSet();
m_vis.addFocusGroup(Visualization.SEARCH_ITEMS, search);
search.addTupleSetListener(new TupleSetListener() {
public void tupleSetChanged(TupleSet t, Tuple[] add, Tuple[] rem) {
m_vis.cancel("animatePaint");
m_vis.run("fullPaint");
m_vis.run("animatePaint");
}
});
}
public void setOrientation(int orientation) {
NodeLinkTreeLayout rtl
= (NodeLinkTreeLayout)m_vis.getAction("treeLayout");
CollapsedSubtreeLayout stl
= (CollapsedSubtreeLayout)m_vis.getAction("subLayout");
switch ( orientation ) {
case Constants.ORIENT_LEFT_RIGHT:
m_nodeRenderer.setHorizontalAlignment(Constants.LEFT);
m_edgeRenderer.setHorizontalAlignment1(Constants.RIGHT);
m_edgeRenderer.setHorizontalAlignment2(Constants.LEFT);
m_edgeRenderer.setVerticalAlignment1(Constants.CENTER);
m_edgeRenderer.setVerticalAlignment2(Constants.CENTER);
break;
case Constants.ORIENT_RIGHT_LEFT:
m_nodeRenderer.setHorizontalAlignment(Constants.RIGHT);
m_edgeRenderer.setHorizontalAlignment1(Constants.LEFT);
m_edgeRenderer.setHorizontalAlignment2(Constants.RIGHT);
m_edgeRenderer.setVerticalAlignment1(Constants.CENTER);
m_edgeRenderer.setVerticalAlignment2(Constants.CENTER);
break;
case Constants.ORIENT_TOP_BOTTOM:
m_nodeRenderer.setHorizontalAlignment(Constants.CENTER);
m_edgeRenderer.setHorizontalAlignment1(Constants.CENTER);
m_edgeRenderer.setHorizontalAlignment2(Constants.CENTER);
m_edgeRenderer.setVerticalAlignment1(Constants.BOTTOM);
m_edgeRenderer.setVerticalAlignment2(Constants.TOP);
break;
case Constants.ORIENT_BOTTOM_TOP:
m_nodeRenderer.setHorizontalAlignment(Constants.CENTER);
m_edgeRenderer.setHorizontalAlignment1(Constants.CENTER);
m_edgeRenderer.setHorizontalAlignment2(Constants.CENTER);
m_edgeRenderer.setVerticalAlignment1(Constants.TOP);
m_edgeRenderer.setVerticalAlignment2(Constants.BOTTOM);
break;
default:
throw new IllegalArgumentException(
"Unrecognized orientation value: "+orientation);
}
m_orientation = orientation;
rtl.setOrientation(orientation);
stl.setOrientation(orientation);
}
public int getOrientation() {
return m_orientation;
}
}
/** the constant for "tree". */
public final static String TREE = "tree";
/** the constant for "tree.nodes". */
public final static String TREE_NODES = "tree.nodes";
/** the constant for "tree.edges". */
public final static String TREE_EDGES = "tree.edges";
/** the constant for "label". */
public final static String LABEL = "label";
/**
* Get the minimum version of Weka, inclusive, the class
* is designed to work with. eg: <code>3.5.0</code>
*
* @return the minimum version
*/
public String getMinVersion() {
return "3.5.9";
}
/**
* Get the maximum version of Weka, exclusive, the class
* is designed to work with. eg: <code>3.6.0</code>
*
* @return the maximum version
*/
public String getMaxVersion() {
return "3.7.0";
}
/**
* Get the specific version of Weka the class is designed for.
* eg: <code>3.5.1</code>
*
* @return the version the plugin was designed for
*/
public String getDesignVersion() {
return "3.5.9";
}
/**
* Get a JMenu or JMenuItem which contain action listeners
* that perform the visualization of the tree in GraphViz's dotty format.
* Exceptions thrown because of changes in Weka since compilation need to
* be caught by the implementer.
*
* @see NoClassDefFoundError
* @see IncompatibleClassChangeError
*
* @param dotty the tree in dotty format
* @param name the name of the item (in the Explorer's history list)
* @return menuitem for opening visualization(s), or null
* to indicate no visualization is applicable for the input
*/
public JMenuItem getVisualizeMenuItem(String dotty, String name) {
JMenuItem result;
final String dottyF = dotty;
final String nameF = name;
result = new JMenuItem("Prefuse tree");
result.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
display(dottyF, nameF);
}
});
return result;
}
/**
* Displays the error.
*
* @param msg the error to display
*/
protected void displayError(String msg) {
JOptionPane.showMessageDialog(null, msg, "Error displaying graph", JOptionPane.ERROR_MESSAGE);
}
/**
* Converts the dotty format to GraphML.
*
* @param dotty the graph in dotty format
* @return the graph in tree XML or null in case of an error
*/
protected String convert(String dotty) {
String result;
DottyToTree d2gml;
d2gml = new DottyToTree();
try {
result = d2gml.convert(dotty);
}
catch (Exception e) {
result = null;
e.printStackTrace();
displayError(e.toString());
}
return result;
}
/**
* Parses the graph in GraphML and returns the built graph.
*
* @param graphml the graph in GraphML
* @return the graph or null in case of an error
*/
protected Tree parse(String graphml) {
ByteArrayInputStream inStream;
Tree result;
try {
inStream = new ByteArrayInputStream(graphml.getBytes());
result = (Tree) new TreeMLReader().readGraph(inStream);
}
catch ( Exception e ) {
result = null;
e.printStackTrace();
displayError(e.toString());
}
return result;
}
/**
* Displays the graph.
*
* @param dotty the graph in dotty format
* @param name the name of the graph
*/
protected void display(String dotty, String name) {
String treeml;
Tree tree;
TreePanel panel;
JFrame frame;
// convert dotty graph
treeml = convert(dotty);
if (treeml == null)
return;
// parse graph
tree = parse(treeml);
if (tree == null)
return;
// display graph
panel = new TreePanel(tree);
frame = new JFrame("Prefuse tree [" + name + "]");
frame.setSize(800, 600);
frame.setContentPane(panel);
frame.setVisible(true);
}
}