Strony

piątek, 22 stycznia 2010

rendering graphs in PHP using GraphViz

Well known and very popular graph rendering software Graphviz has got many different language bindings and is widely used in many different areas of maths and computing. I don't have to speak more. It just renders all those graphs in easy way using very simple but powerful language.

Amongst various tools which are using Graphviz there is also a PHP package in PEAR — Image_GraphViz, which has got only one pitfall — it cannot render cluster (a subgraph) inside a cluster (at least not in 1.2.1 version). So, I modified it a bit to make it possible:

require_once("System.php");
/**
* @class graphviz
* @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmail.com)
* @brief class for making Graphviz plots online
*/
class graphviz {
var $directed = True;
var $_returnFalseOnError = true;
var $graph = array( "name" => "G",
"__attrs__" => array(),
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() );
var $fmt = "svg";
var $dotCommand = "/usr/bin/dot";
var $neatoCommand = "/usr/bin/neato";
/**
* @brief c'tor
* @param $name graph name
* @param $fmt output format
*/
public function __construct( $name=null, $fmt=null, $attr=null ) {
if ( $attr == null ) $attr = array();
$this->graph["_attrs__"] = $attr;
if ( $name != null && is_string($name) ) {
$this->graph["name"] = $name;
} else {
$name = "G";
}
if ( $fmt != null && is_string($fmt) ) {
$this->fmt = $fmt;
}
$this->graph = array( "name" => $name,
"__attrs__" => $attr,
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() );
}
/**
* @brief sets directed/undirected flag for the graph.
* @param $directed Directed (TRUE) or undirected (FALSE) graph.
*/
public function setDirected( $directed=True ) {
if (is_bool($directed)) {
$this->directed = $directed;
}
}
/**
* @brief cluster finder
* @param $clusterName name of cluster to find
* @param $where reference to array
*/
public function &findCluster( $clusterName, &$where=null) {
if ( $where == null ) {
$where =& $this->graph;
}
if ( isset( $where["__clusters__"][$clusterName] ) ) {
return $where["__clusters__"][$clusterName];
} else {
foreach ( $where["__clusters__"] as $key => &$cluster ) {
return $this->findCluster( $clusterName, &$cluster );
}
}
return null;
}
/**
* @brief add subcluster to graph
* @param $id new cluster id
* @parm $attr array with attributes
* @param $toCluster parent subcluster
*/
public function addCluster( $id, $attr=null, $toCluster=null ) {
if ( $attr == null) $attr = array();
if ( $toCluster == null ) {
if ( ! isset( $this->graph["__clusters__"][$id] ) ) {
$this->graph["__clusters__"][$id] = array( "__attrs__" => $attr,
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() ) ;
}
} else {
$where =& $this->findCluster( $toCluster );
if ( $where != null ) {
$where["__clusters__"][$id] = array( "__attrs__" => $attr,
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() ) ;
}
}
}
/**
* @brief add node to graph
* @param $id node id
* @param $attr array with node attributes
* @param $cluster parent cluster
*/
public function addNode( $id, $attr=null, $cluster=null ) {
if ( $attr == null ) $attr = array();
if ( $cluster != null ) {
$where =& $this->findCluster( $cluster );
if ( $where != null ) {
$where["__nodes__"][$id] = $attr;
} else {
$this->addCluster($cluster);
$this->graph["__clusters__"][$cluster]["__nodes__"][$id] = $attr;
}
} else {
$this->graph["__nodes__"][$id] = $attr;
}
}
/**
* @brief connect to nodes
* @param $from begin node
* @param $to end node
* @param $attr edge attributes
*/
public function addEdge( $from, $to, $attr=null ) {
$id = $from . "-->" . $to;
if ( !isset( $this->graph["__edges__"][$id]) ) {
$this->graph["__edges__"][$id] = array( "__from__" => $from, "__to__" => $to, "__attrs__" => array() );
if ( is_array( $attr ) ) {
$this->graph["__edges__"][$id]["__attrs__"] = $attr;
}
} else {
if ( is_array($attr) ) {
$this->graph["__edges__"][$id]["__attrs__"] = array_merge($attr, $this->graph["__edges__"][$id]["__attrs__"] );
}
}
}
/**
* @brief transform internal subclusters representation to dot language
* @param where location to parse
* @return string with GraphViz markup
*/
private function __parseClusters( $where = null ) {
$out = "";
if ( $where == null ) {
$where = $this->graph;
}
foreach ( $where["__clusters__"] as $clusterName => $cluster ) {
$out .= "subgraph cluster_".$clusterName. " {\n";
// attributes
foreach ( $cluster["__attrs__"] as $attr => $attrValue ) {
$attributeList[] = $attr.'="'.$attrValue.'"';
}
if (!empty($attributeList) ) {
$out .= implode(';', $attributeList).";\n";
}
// nodes
foreach ( $cluster["__nodes__"] as $nodeName => $attr ) {
foreach ( $attr as $attrName => $attrValue ) {
$nodeAttr[] = $attrName . "=\"" . $attrValue . "\"";
}
$out .= $nodeName;
if ( !empty($nodeAttr) ) {
$out .= " [ " . implode(',', $nodeAttr) . " ]";
}
$out .= ";\n";
}
$out .= $this->__parseClusters( $cluster );
$out .= "}\n";
}
return $out;
}
/**
* @brief transform internal graph representation to dot language
* @return string GraphViz markup
*/
public function parse() {
$parsedGraph = $this->directed ? "digraph " : "graph ";
if ( isset($this->graph["name"]) &&
is_string($this->graph["name"]) ) {
$parsedGraph .= $this->graph["name"] . " {\n";
} else {
$parsedGraph .= "G {\n";
}
if (isset($this->graph["__attrs__"])) {
foreach ($this->graph["__attrs__"] as $key => $value) {
$attributeList[] = $key . '="' . $value . '"';
}
if ( !empty($attributeList) ) {
$parsedGraph .= "graph [ ".implode(",", $attributeList) . " ];\n";
}
}
// subclusters
$parsedGraph .= $this->__parseClusters();
foreach ( $this->graph["__nodes__"] as $nodeName => $attr ) {
foreach ( $attr as $attrName => $attrValue ) {
$nodeAttr[] = $attrName . "=\"" . $attrValue . "\"";
}
$parsedGraph .= $nodeName;
if ( !empty($nodeAttr) ) {
$parsedGraph .= " [ " . implode(',', $nodeAttr) . " ]";
}
$parsedGraph .= ";\n";
}
// edges
foreach ( $this->graph["__edges__"] as $id => $edge ) {
$from = $edge["__from__"];
$to = $edge["__to__"];
$egdeAttributes = $edge["__attrs__"];
foreach ( $edgeAttributes as $attrName => $attrValue ) {
$edgeAttrList[] = $attrName."=\"".$attrValue."\"";
}
$parsedGraph .= $from . " -> " . $to;
if ( !empty($edgeAttrList) ) {
$parsedGraph .= " [ " . implode(",", $edgeAttrList) . " ]";
}
$parsedGraph .= ";\n";
}
// end of graph
$parsedGraph .= "}\n";
return $parsedGraph;
}
function fetch($format = 'svg', $command = null) {
$file = $this->saveParsedGraph();
if (!$file || PEAR::isError($file)) {
return $file;
}
$outputfile = $file . '.' . $format;
$rendered = $this->renderDotFile($file, $outputfile, $format,
$command);
if ($rendered !== true) {
return $rendered;
}
@unlink($file);
$fp = fopen($outputfile, 'rb');
if (!$fp) {
if ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError('Could not read rendered file');
return $error;
}
$data = fread($fp, filesize($outputfile));
fclose($fp);
@unlink($outputfile);
return $data;
}
function saveParsedGraph($file = '') {
$parsedGraph = $this->parse();
if (!empty($parsedGraph)) {
if (empty($file)) {
$file = System::mktemp('graph_');
}
if ($fp = @fopen($file, 'wb')) {
@fputs($fp, $parsedGraph);
@fclose($fp);
return $file;
}
}
if ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError('Could not save graph');
return $error;
}
function renderDotFile($dotfile, $outputfile, $format = 'svg', $command = null) {
if (!file_exists($dotfile)) {
if ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError('Could not find dot file');
return $error;
}
$oldmtime = file_exists($outputfile) ? filemtime($outputfile) : 0;
switch ($command) {
case 'dot':
case 'neato':
break;
default:
$command = $this->directed ? 'dot' : 'neato';
}
$command_orig = $command;
$command = $this->binPath.(($command == 'dot') ? $this->dotCommand : $this->neatoCommand);
$command .= ' -T'.escapeshellarg($format) .' -o'.escapeshellarg($outputfile)
.' '.escapeshellarg($dotfile).' 2>&1';
exec($command, $msg, $return_val);
clearstatcache();
if (file_exists($outputfile) &&
filemtime($outputfile) > $oldmtime
&& $return_val == 0 ) {
return true;
} elseif ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError($command_orig.' command failed: '.implode("\n", $msg));
return $error;
}
/**
*
* Outputs image of the graph in a given format
* This methods send HTTP headers
* @param string $format Format of the output image. This may be one
* of the formats supported by GraphViz.
* @param string $command "dot" or "neato"
*
* @return boolean TRUE on success, FALSE or PEAR_Error otherwise
*/
public function image( $format = 'svg', $command = null ) {
$file = $this->saveParsedGraph();
if (!$file || PEAR::isError($file)) {
return $file;
}
$outputfile = $file . '.' . $format;
$rendered = $this->renderDotFile($file, $outputfile, $format, $command);
if ($rendered !== true) { return $rendered; }
$sendContentLengthHeader = true;
switch (strtolower($format)) {
case 'gif':
case 'png':
case 'bmp':
case 'jpeg':
case 'tiff':
header('Content-Type: image/' . $format);
break;
case 'tif':
header('Content-Type: image/tiff');
break;
case 'jpg':
header('Content-Type: image/jpeg');
break;
case 'ico':
header('Content-Type: image/x-icon');
break;
case 'wbmp':
header('Content-Type: image/vnd.wap.wbmp');
break;
case 'pdf':
header('Content-Type: application/pdf');
break;
case 'mif':
header('Content-Type: application/vnd.mif');
break;
case 'vrml':
header('Content-Type: application/x-vrml');
break;
case 'svg':
header('Content-Type: image/svg+xml');
break;
case 'plain':
case 'plain-ext':
header('Content-Type: text/plain');
break;
default:
header('Content-Type: application/octet-stream');
$sendContentLengthHeader = false;
}
if ($sendContentLengthHeader) {
header('Content-Length: ' . filesize($outputfile));
}
$return = true;
if (readfile($outputfile) === false) {
$return = false;
}
@unlink($outputfile);
return $return;
}
}
class test_graphviz {
public function __construct($fmt=null) {
$this->gr = new graphviz( "a", "svg", array( "bgcolor" => "#ffff00") );
$this->gr->addCluster( "main", array( "label" => "main", "labelloc"=>"t", "bgcolor" => "#ff0000", "labeljust"=>"l" ), null );
$this->gr->addCluster( "sub", array( "label" => "sub", "labelloc"=>"t", "bgcolor" => "#0000ff", "labeljust"=>"l" ), "main" );
$this->gr->addNode( "node_in_graph", array("shape" => "box", "color" => "#00ff00") );
$this->gr->addNode( "node_in_main", array("shape" => "box"), "main" );
$this->gr->addNode( "node_in_sub", array("shape" => "box"), "sub" );
$this->gr->addEdge( "node_in_main", "node_in_graph" );
$this->gr->addEdge( "node_in_main", "node_in_sub" );
$this->gr->addEdge( "node_in_sub", "node_in_graph" );
if ( $fmt == null ) {
$this->gr->image();
} else {
echo "
---dot---\n";
print_r( $this->gr->graph );
echo "
";
echo "
---graph---\n";
echo $this->gr->parse();
echo "
";
}
}
}
if ( isset($_GET["dot"] ) ) {
$t = new test_graphviz( "dot" );
}
if ( isset($_GET["test"] ) ) {
$t = new test_graphviz();
}
view raw Render.php hosted with ❤ by GitHub


Here is the results of running above example on apache (it renders image in svg format, so not all web browsers are able to display it correctly):



Test Graph in SVG

I was very happy when I'd figured out that my changes went into the official Image_GraphViz 1.3.0RC3. Good luck folks!

Brak komentarzy:

Prześlij komentarz