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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
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