Use (c) instead of the actual copyright symbol to avoid the really annoying character...
[isso.git] / GraphLine.php
1 <?php
2 /*=====================================================================*
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright (c)2002-2007 Blue Static
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version 2 of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 /**
23 * Graphing System: Line Graph (GraphLine.php)
24 *
25 * @package ISSO
26 */
27
28 require_once('ISSO/Graph.php');
29
30 /**
31 * Graphing System: Line Graph
32 *
33 * This creates a line graph from a set of data; each point requires
34 * a line name (because this supports multi-line graphing), an x-value,
35 * and a y-value. It creates PNG images.
36 *
37 * @author Blue Static
38 * @copyright Copyright (c)2002 - 2007, Blue Static
39 * @package ISSO
40 *
41 */
42 class BSGraphLine extends BSGraph
43 {
44 /**
45 * Graphing dataset; 4D array
46 * array(array(name, array(array(xval, yval))), color)
47 * @var string
48 */
49 protected $dataset = array();
50
51 /**
52 * Array of data points that are used to calculate the standard deviation
53 * @var array
54 */
55 private $piles = array(0 => array(), 1 => array());
56
57 /**
58 * The names of the axes
59 * @var array
60 */
61 private $axis = array(0 => 'X Axis', 1 => 'Y Axis');
62
63 /**
64 * Number of ticks to display on the axes
65 * @var integer
66 */
67 private $ticks = 10;
68
69 // ###################################################################
70 /**
71 * Does the actual graphing and returns a byte stream of a PNG image
72 *
73 * @return string Byte stream
74 */
75 public function graph()
76 {
77 $colors = $this->_primeColors();
78 $this->_paintCanvas();
79
80 // draw the axes
81 $originx = self::PADDING + imagefontwidth(1) + self::SPACING + imagefontwidth(3) + self::PADDING;
82 $originy = $this->dimensions['height'] - (self::PADDING + imagefontheight(1) + self::SPACING + imagefontheight(3) + self::SPACING);
83 $endx = $this->dimensions['width'] - self::PADDING - 150 - self::PADDING;
84 $endy = 40;
85 $length = $endx - $originx;
86 $height = $originy - $endy;
87 imageline($this->image, $originx, $originy, $endx, $originy, $colors['grey']);
88 imageline($this->image, $originx, $originy, $originx, $endy, $colors['grey']);
89
90 // just to give us some padding
91 $this->ticks++;
92
93 // calculates the standard deviation of the two piles to determine the x and y intervals
94 $xmin = min($this->piles[0]);
95 $xmax = max($this->piles[0]);
96 $xint = round(($xmax - $xmin) / $this->ticks);
97 $xmin = ($xmin - $xint < 0 ? 0 : $xmin - $xint);
98 $xmax = $xmax + $xint;
99
100 $ymin = min($this->piles[1]);
101 $ymax = max($this->piles[1]);
102 $yint = round(($ymax - $ymin) / $this->ticks);
103 $ymin = ($ymin - $yint < 0 ? 0 : $ymin - $yint);
104 $ymax = $ymax + $yint;
105
106 // label the axes
107 imagestring($this->image, 3, $length / 2, $this->dimensions['height'] - self::SPACING - imagefontheight(3), $this->axis[0], $colors['black']);
108 imagestringup($this->image, 3, self::SPACING, $height / 2 + $endy, $this->axis[1], $colors['black']);
109
110 // score the axes
111 $count = 0;
112 for ($i = $originx; $i <= $endx; $i += ($length / $this->ticks))
113 {
114 imageline($this->image, $i - self::SPACING, $originy + self::SPACING, $i + self::SPACING, $originy - self::SPACING, $colors['grey']);
115 imagestring($this->image, 1, $i, $originy + self::PADDING, round($count), $colors['black']);
116 $count += $xint;
117 }
118 $count = 0;
119 for ($i = $originy; $i >= $endy; $i -= ($height / $this->ticks))
120 {
121 imageline($this->image, $originx, $i, $endx, $i, $colors['grey']);
122 imagestring($this->image, 1, self::SPACING + self::SPACING + self::PADDING + self::SPACING, $i - self::SPACING, round($count), $colors['black']);
123 $count += $yint;
124 }
125
126 // draw the legend
127 $legy = $endy + self::SPACING; // "cursor" y-coord for the legend
128 $legx = $endx + self::PADDING; // x-coord for the legend BORDER
129 $legex = $this->dimensions['width'] - self::PADDING; // end x-coord for the legend BORDER
130 imageline($this->image, $legx, $endy, $legex, $endy, $colors['black']); // top legend border
131
132 // go through and plot each dataset
133 foreach ($this->dataset AS $data)
134 {
135 // plot each point and connect the dots
136 $oldpoint = null;
137 foreach ($data[1] AS $points)
138 {
139 $xcord = $originx + ($points[0] * ($length / $xmax));
140 $ycord = $originy - ($points[1] * ($height / $ymax));
141 imagefilledellipse($this->image, $xcord, $ycord, 5, 5, $data[2]);
142 if ($oldpoint)
143 {
144 imageline($this->image, $xcord, $ycord, $oldpoint[0], $oldpoint[1], $data[2]);
145 }
146 $oldpoint = array($xcord, $ycord);
147 }
148
149 // draw the legend bit
150 $box = array(
151 $legx + 1 + self::SPACING, $legy, // top left
152 $legx + 1 + self::SPACING, $legy + self::PADDING, // bottom left
153 $legx + 11 + self::SPACING, $legy + self::PADDING, // bottom right
154 $legx + 11 + self::SPACING, $legy // top right
155 );
156 imagefilledpolygon($this->image, $box, 4, $data[2]);
157 imagestring($this->image, 2, $legx + 11 + self::SPACING + self::SPACING, $legy - 1, $data[0], $colors['black']);
158 $legy += self::PADDING + self::SPACING;
159 }
160
161 // finish the legend border
162 imageline($this->image, $legx, $legy, $legex, $legy, $colors['black']); // bottom
163 imageline($this->image, $legx, $endy, $legx, $legy, $colors['black']); // left
164 imageline($this->image, $legex, $endy, $legex, $legy, $colors['black']); // right
165
166 return $this->_imageFlush();
167 }
168
169 // ###################################################################
170 /**
171 * Adds a "line" with a given name and a set of datapoints in the form
172 * array(x, y)
173 *
174 * @param string The line's name
175 * @param array Array of array(x,y) as data points
176 */
177 public function addDataSet($name, $points)
178 {
179 $this->_addPoints($points);
180 $this->_sortPoints($points);
181 $this->dataset[] = array($name, $points, $this->_fetchColor());
182 }
183
184 // ###################################################################
185 /**
186 * This does the same thing as addDataSet(), except the client code
187 * can specify the color in the form of array(R, G, B)
188 *
189 * @param string The line's name
190 * @param array Array of array(x,y) as data points
191 * @param array A color in the form of 3 RGB points
192 */
193 public function addDataSetColor($name, $points, $color)
194 {
195 $this->_addPoints($points);
196 $this->_sortPoints($points);
197 $this->dataset[] = array($name, $points, imagecolorallocate($this->image, $color[0], $color[1], $color[2]));
198 }
199
200 // ###################################################################
201 /**
202 * Adds a set of points to the piles and ensures that they are all valid
203 *
204 * @param array Points to add
205 */
206 private function _addPoints($points)
207 {
208 $xpairs = array();
209 foreach ($points AS $point)
210 {
211 if (isset($xpairs["$point[0]"]))
212 {
213 trigger_error('You cannot have more than one of the same x-values for a given data set');
214 }
215 $xpairs["$point[0]"] = $point[0];
216 $this->piles[0][] = $point[0];
217 $this->piles[1][] = $point[1];
218 }
219 }
220
221 // ###################################################################
222 /**
223 * Sorts an array of points using quick sort so they're in x-increasing
224 * order
225 *
226 * @param array Array of points
227 */
228 private function _sortPoints(&$points)
229 {
230 $this->_quickSortPoints($points, 0, sizeof($points) - 1);
231 }
232
233 // ###################################################################
234 /**
235 * Quicksort function for sorting function
236 *
237 * @param array Array of points
238 * @param integer Lower bound
239 * @param integer Upper bound
240 */
241 private function _quickSortPoints(&$points, $low, $high)
242 {
243 if (($high - $low) > 1)
244 {
245 $partition = $this->_partitionPoints($points, $low, $high);
246 $this->_quickSortPoints($points, $low, $partition);
247 $this->_quickSortPoints($points, $partition + 1, $high);
248 }
249 }
250
251 // ###################################################################
252 /**
253 * Quicksort partitioner: returns the index of the pivot element where
254 * all x-coords on the left side of pivot are less than or equal to
255 * pivot, and all x-coords are higher to the right
256 *
257 * @param array Array of points
258 * @param integer Lower bound
259 * @param integer Upper bound
260 *
261 * @return integer Pivot index
262 */
263 private function _partitionPoints(&$points, $low, $high)
264 {
265 $pivot = $low;
266 for ($unsorted = $low + 1; $unsorted <= $high; $unsorted++)
267 {
268 if ($points[$unsorted][0] < $points[$pivot][0])
269 {
270 $temp = $points[$pivot];
271 $points[$pivot] = $points[$unsorted];
272 $points[$unsorted] = $points[$pivot + 1];
273 $points[$pivot + 1] = $temp;
274 $pivot++;
275 }
276 }
277 return $pivot;
278 }
279
280 // ###################################################################
281 /**
282 * Sets the titles of the two axes
283 *
284 * @param string X-axis name
285 * @param string Y-axis name
286 */
287 public function setAxes($xaxis, $yaxis)
288 {
289 $this->axis[0] = $xaxis;
290 $this->axis[1] = $yaxis;
291 }
292 }
293
294 ?>