Skip to content

Commit

Permalink
Add support for dependency arrows to Gantt charts.
Browse files Browse the repository at this point in the history
  • Loading branch information
goat1000 committed Jun 16, 2022
1 parent 925341e commit ec35009
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 18 deletions.
104 changes: 104 additions & 0 deletions Arrow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/**
* Copyright (C) 2022 Graham Breach
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* For more information, please contact <[email protected]>
*/

namespace Goat1000\SVGGraph;

/**
* A class for drawing arrows
*/
class Arrow {

protected $a;
protected $b;
protected $head_size = 7;
protected $head_colour = '#000';

public function __construct(Point $a, Point $b)
{
$this->a = $a;
$this->b = $b;
}

/**
* Sets the arrow head size (min 2 pixels)
*/
public function setHeadSize($size)
{
$this->head_size = max(2, $size);
}

public function setHeadColour($colour)
{
$this->head_colour = $colour;
}

/**
* Returns the arrow head as the ID of a <marker> element
*/
protected function getArrowHead($graph)
{
$sz = new Number($this->head_size);
$point = 75; // sharpness of arrow
$marker = [
'viewBox' => "0 0 {$point} 100",
'markerWidth' => $sz,
'markerHeight' => $sz,
'refX' => $point,
'refY' => 50,
'orient' => 'auto',
];
$pd = new PathData('M', 0, 0, 'L', $point, 50, 'L', 0, 100, 'z');
$path = [
'd' => $pd,
'stroke' => $this->head_colour,
'fill' => $this->head_colour,
];
$marker_content = $graph->element('path', $path);
return $graph->defs->addElement('marker', $marker, $marker_content);
}

/**
* Returns the PathData for an arrow line
*/
protected function getArrowPath()
{
return new PathData('M', $this->a, $this->b);
}

/**
* Returns the arrow element
*/
public function draw($graph, $style = null)
{
$head_id = $this->getArrowHead($graph);
$p = $this->getArrowPath();

$path = [
'd' => $p,
'marker-end' => 'url(#' . $head_id . ')',
'fill' => 'none',
];
if(is_array($style))
$path = array_merge($style, $path);
return $graph->element('path', $path);
}
}

6 changes: 6 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version 3.15 (16/06/2022)
------------
- Added dependency arrow support to Gantt charts.
- Added gantt_units option for tasks in hours and minutes.
- Added grid_clip_overlap_* options for adjusting line graph clipping.

Version 3.14 (02/05/2022)
------------
- Added Gantt chart.
Expand Down
19 changes: 18 additions & 1 deletion Defs.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Copyright (C) 2019 Graham Breach
* Copyright (C) 2019-2022 Graham Breach
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
Expand Down Expand Up @@ -32,6 +32,7 @@ class Defs {
private $patterns = null;
private $symbols = null;
private $filters = null;
private $elements = [];

public function __construct(&$graph)
{
Expand All @@ -46,6 +47,22 @@ public function add($def)
$this->defs[] = $def;
}

/**
* Adds an element to the defs, returning its ID, or the ID
* of an existing def with same content
*/
public function addElement($element, $attrs, $content = '')
{
$ehash = hash('md5', $element . ':' . serialize($attrs) . ':' . $content);
if(isset($this->elements[$ehash]))
return $this->elements[$ehash];

$attrs['id'] = $this->graph->newID();
$this->elements[$ehash] = $attrs['id'];
$this->add($this->graph->element($element, $attrs, null, $content));
return $attrs['id'];
}

/**
* Return the defs block, or an empty string if none
*/
Expand Down
89 changes: 89 additions & 0 deletions GanttArrow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
/**
* Copyright (C) 2022 Graham Breach
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* For more information, please contact <[email protected]>
*/

namespace Goat1000\SVGGraph;

/**
* A class for drawing arrows
*/
class GanttArrow extends Arrow {

protected $type = 0;
protected $vsplit = false;
protected $space = 10;

public function __construct(Point $a, Point $b, $wa, $ha, $wb, $hb, $type, $sp)
{
switch($type) {

case 'SS':
$this->vsplit = ($a->x < $b->x);
break;

case 'FF':
$a->x += $wa;
$b->x += $wb;
$this->vsplit = ($a->x > $b->x);
break;

case 'SF':
$b->x += $wb;
$this->vsplit = ($a->x < $b->x);
break;

case 'FS':
default:
$a->x += $wa;
$this->vsplit = ($a->x > $b->x);
}
$a->y += $ha;

parent::__construct($a, $b);
$this->type = $type;
$this->space = max(5, $sp);
}

/**
* Returns the PathData for an arrow line
*/
protected function getArrowPath()
{
$p = new PathData('M', $this->a);
$dx = $this->b->x - $this->a->x;
$dy = $this->b->y - $this->a->y;

if($dx && $this->vsplit) {
$v1 = new Number($dy - $this->space);
$v2 = new Number($this->space);
$p->add('v', $v1);
$p->add('h', new Number($dx));
$p->add('v', $v2);

} else {
// if horizontal very small, ignore it
if(abs($dx) > 0.1)
$p->add('h', new Number($dx));
$p->add('v', new Number($this->b->y - $this->a->y));
}
return $p;
}
}

80 changes: 71 additions & 9 deletions GanttChart.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class GanttChart extends HorizontalBarGraph {
protected $start_date = null;
protected $end_date = null;
protected $auto_format = true;
protected $bar_list = [];

public function __construct($w, $h, array $settings, array $fixed_settings = [])
{
Expand Down Expand Up @@ -619,6 +620,8 @@ protected function drawBar(DataItem $item, $index, $start = 0, $axis = null,
$bar['x'], $bar['y'], $bar['width'], $bar['height'], $label);
}

$depends = $this->drawDependencies($item, $index, $dataset, $bar);

if($this->semantic_classes)
$element['class'] = 'series' . $dataset;

Expand All @@ -628,12 +631,15 @@ protected function drawBar(DataItem $item, $index, $start = 0, $axis = null,
if($this->show_context_menu)
$this->setContextMenu($element, $dataset, $item, $label_shown);

$task_entry = '';
if($item->milestone) {
$m = new MarkerShape($element, 'above');
return $m->draw($this);
$task_entry .= $m->draw($this);
} else {
$bar_part = $this->element($bar_type, $element, null, $bar_content);
$task_entry .= $this->getLink($item, $item->key, $bar_part);
}
$bar_part = $this->element($bar_type, $element, null, $bar_content);
return $this->getLink($item, $item->key, $bar_part);
return $task_entry . $depends;
}

/**
Expand Down Expand Up @@ -670,7 +676,7 @@ protected function getBarColours(DataItem $item, $index, $dataset)
/**
* Returns the attributes of a bar
*/
protected function getBar(DataItem $item, $index, $dataset, $bar)
protected function getBar(DataItem $item, $index, $dataset, &$bar)
{
list($colour_incomplete, $colour_complete) = $this->getBarColours($item, $index, $dataset);
$round = max($this->getItemOption('bar_round', $dataset, $item), 0);
Expand All @@ -685,7 +691,7 @@ protected function getBar(DataItem $item, $index, $dataset, $bar)
$corner_height = $this->getItemOption('gantt_group_corner_height', $dataset, $item, 'corner_height');
}

$element =& $bar;
$element = $bar;

// group bar has downward pointing corners
if($corner_height && $corner_width) {
Expand All @@ -703,11 +709,15 @@ protected function getBar(DataItem $item, $index, $dataset, $bar)
$p->add('l', -$corner_width, $corner_height);
$p->add('z');
$path['d'] = $p;
$element =& $path;
$element = $path;

// update $bar
$bar['y'] -= $corner_height / 2;
$bar['height'] += $corner_height;
} else {

$bar['element'] = 'rect';
$bar['content'] = null;
$element['element'] = 'rect';
$element['content'] = null;
}
if($item->complete >= 100) {
$element['fill'] = $colour_complete;
Expand All @@ -726,7 +736,7 @@ protected function getBar(DataItem $item, $index, $dataset, $bar)

// % complete
$c = $this->getClippers($bar['x'], $bar['y'], $bar['width'],
$bar['height'] + $corner_height, $item->complete);
$bar['height'], $item->complete);
$bar_parts = '';
$b1 = $element;
$b1['fill'] = $colour_complete;
Expand Down Expand Up @@ -859,6 +869,58 @@ protected function getPointer(DataItem $item, $index, $dataset, $bar)
return $marker;
}

/**
* Draws dependency arrows
*/
protected function drawDependencies(&$item, $index, $dataset, $bar)
{
// add this bar to the list so others can draw arrows to it
$this->bar_list[$item->key] = $bar;
if(!isset($item->depends))
return '';

$arrows = '';
$depends = is_array($item->depends) ? $item->depends : [$item->depends];
$dtype = is_array($item->depends_type) ? $item->depends_type : [$item->depends_type];

$head_size = $this->getItemOption('gantt_depends_head_size', $dataset,
$item, 'depends_head_size');
$stroke_width = min(10, max(0.1,
$this->getItemOption('gantt_depends_stroke_width', $dataset, $item, 'depends_stroke_width')));
$cg = new ColourGroup($this, $item, $index, $dataset, 'gantt_depends_colour', null, 'depends_colour');
$colour = $cg->stroke();
$dash = $this->getItemOption('gantt_depends_dash', $dataset, $item, 'depends_dash');
$opacity = min(1, max(0,
$this->getItemOption('gantt_depends_opacity', $dataset, $item, 'depends_opacity')));

$group_style = [ 'stroke' => $colour, ];
if($stroke_width != 1)
$group_style['stroke-width'] = $stroke_width;
if(!empty($dash))
$group_style['stroke-dasharray'] = $dash;
if($opacity < 1)
$group_style['opacity'] = $opacity;

foreach($depends as $k => $d) {
if(!isset($this->bar_list[$d]))
break;

$dbar = $this->bar_list[$d];
$arrow = new GanttArrow(new Point($dbar['x'], $dbar['y']),
new Point($bar['x'], $bar['y']),
$dbar['width'], $dbar['height'],
$bar['width'], $bar['height'],
isset($dtype[$k]) ? $dtype[$k] : 'FS',
$this->calculated_bar_space);

$arrow->setHeadSize($head_size);
$arrow->setHeadColour($colour);
$arrows .= $arrow->draw($this);
}
$arrows = $this->element('g', $group_style, null, $arrows);
return $arrows;
}

/**
* Tooltips are a little more complicated on Gantt chart
*/
Expand Down
Loading

0 comments on commit ec35009

Please sign in to comment.