1<?php
2/* $Id: data_table.php 1000 2011-08-06 01:19:27Z lbayuk $
3phplot / contrib / data_table.php: Draw a table of data values
4
5     Copyright (c) 2011, lbayuk -at- users.sourceforge.net
6     All rights reserved.
7
8     Redistribution and use in source and binary forms, with or without
9     modification, are permitted provided that the following conditions are met:
10
11         * Redistributions of source code must retain the above copyright
12     notice, this list of conditions and the following disclaimer.
13         * Redistributions in binary form must reproduce the above copyright
14     notice, this list of conditions and the following disclaimer in the
15     documentation and/or other materials provided with the distribution.
16
17     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18     AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19     IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20     ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21     LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22     CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23     SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24     INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25     CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26     ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27     POSSIBILITY OF SUCH DAMAGE.
28
29This function draws a data table. That is, using PHP GD functions, it draws
30a table containing elements from a 2-dimensional array of text and number data.
31
32This is meant to be used as a PHPlot callback, but does not use PHPlot.
33It takes 2 arguments: a PHP GD image resource, and an array of settings. It
34draws the table into the image and returns TRUE.  On error, it uses
35trigger_error(); if an error handler returns, it returns FALSE.
36
37The following keys are required in the settings array:
38
39    'data' => A PHPlot-style data array. Array[0..n-1] of rows, each
40      row entry is an array with values (strings or numbers). The
41      'headers' entry (see below) determines which columns are used in
42      the table. Other settings control how the values are formatted and
43      drawn in the table.
44      Note: Multi-line string values (with newlines) do not work.
45
46    'headers' => Array of headers for the data array.
47      There must be an entry for each data array column. The entry is the
48      header label for that column.  NULL means do not include this data array
49      column in the table.
50      Header labels are centered within the header row of the table.
51      Multi-line entries (with newlines) are not supported and do not work.
52
53The following keys are optional in the settings array:
54
55    'position' => Array (x, y) giving the GD coordinates for the upper left
56      corner of the table. The default is (0,0), which is the upper left
57      corner of the image.
58
59    'height' => Height in pixels of the table.
60       If not given or empty or 0, the table height is calculated as the minimum
61       value, taking into account the number of rows, font height, and
62       cellpadding value.
63
64    'width' => Width in pixels of the table
65       If not given or empty or 0, the table width is calculated to just fit
66       the widest value in each column (after formatting).
67       If width is not provided, column_widths is ignored.
68
69    'column_widths' => Array of relative column width weights.
70       For example: array(4,2,1,1) means the 1st column is 2 times
71       as wide as the 2nd, and 4 times as wide as the 3rd and 4th.
72       If missing, all columns get equal width. Ignored if the overall
73       table width is not given.
74
75    'column_formats' => Array of printf formats for each column.
76       An empty string means no formatting for that column - the value is
77       drawn into the table using PHP's defaults.
78       If missing, there is no formatting for any column.
79
80    'column_alignments' => Array of L C or R for horizontal alignment, meaning
81       left align, center, or right align.
82       An empty string means automatic: align right if numeric, else left.
83       Default if missing is automatic for all columns.
84
85    'font' => GD font number 1-5. 1 is smallest, 5 is largest. Default is 2.
86
87    'color' => Array with R, G, B color specification. Default is (0,0,0)=black.
88            White would be (255,255,255). This color is used for grid lines
89            and text in the table.
90
91    'cellpadding' => Line to text gap in pixels. Default is 4.
92
93Note that the arrays 'column_widths', 'column_formats', and 'column_alignments'
94have one entry for each column in the table which will be drawn. This is
95different from 'headers', which has one column for each column in the
96data array. These sizes will be the same only if 'headers' has no NULL
97entries - that is, no data array columns are being skipped.
98
99*/
100function draw_data_table($img, $settings)
101{
102    // Apply defaults, then extract all settings as variables named 'o_*':
103    extract(array_merge(array(
104            'color' => array(0, 0, 0),  // Default black
105            'font' => 2,                // Default GD font
106            'position' => array(0, 0),  // Default to upper left corner
107            'width' => 0,               // Default auto width calculation
108            'height' => 0,              // Default auto height calculation
109            'cellpadding' => 4,         // Default line-to-text spacing
110        ), $settings), EXTR_PREFIX_ALL, 'o');
111    list($x, $y) = $o_position; // Expand to separate variables
112
113    // Check for mandatory settings:
114    if (!isset($o_data, $o_headers)) {
115        trigger_error("draw_data_table error: 'headers' and 'data' are required");
116        return FALSE; // In case error handler returns
117    }
118
119    // Font and color setup:
120    $char_width = imagefontwidth($o_font);
121    $char_height = imagefontheight($o_font);
122    $color = imagecolorresolve($img, $o_color[0], $o_color[1], $o_color[2]);
123    $pad2 = 2 * $o_cellpadding; // Pad all 4 sides of cells
124
125    // Calculate the number of rows and columns in the table:
126    $n_rows = count($o_data) + 1; // Add 1 for header row
127    // Count non-skipped columns:
128    $n_cols = 0;
129    foreach ($o_headers as $h) if (!is_null($h)) $n_cols++;
130    // Number of columns in the data array and in $o_headers:
131    $n_data_cols = count($o_headers);
132
133    // Default column weights so all columns have equal width.
134    if (empty($o_column_widths))
135        $o_column_widths = array_fill(0, $n_cols, 1);
136
137    // Default column formats to no formatting:
138    if (empty($o_column_formats))
139        $o_column_formats = array_fill(0, $n_cols, '');
140
141    // Default column alignments to auto align:
142    if (empty($o_column_alignments))
143        $o_column_alignments = array_fill(0, $n_cols, '');
144
145    // Make sure there are the right number of entries.
146    if (count($o_column_widths) != $n_cols
147     || count($o_column_formats) != $n_cols
148     || count($o_column_alignments) != $n_cols) {
149        trigger_error("draw_data_table error: Mismatch in size of column spec arrays");
150        return FALSE; // In case error handler returns
151    }
152
153    // If the table height is not supplied, calculate the space needed.
154    if (empty($o_height))
155        $o_height = $n_rows * ($char_height + $pad2);
156    // Then calculate the height of each row.
157    $row_height = $o_height / $n_rows;
158
159    // If the table width is not supplied, calculate the space needed based
160    // on the widest value for each column (including header). The column width
161    // factors are ignored, since each column will be as wide as needed.
162    if (empty($o_width)) {
163        $o_width = 0;
164        $col = 0; // Index to unskipped columns
165        for ($i = 0; $i < $n_data_cols; $i++) { // Index to all columns
166            if (is_null($o_headers[$i])) continue; // Skip column
167            // Find the longest string in this column, post-formatting.
168            $len = strlen($o_headers[$i]); // Start with the header
169            for ($row = 1; $row < $n_rows; $row++) {
170                if (($cell = $o_data[$row - 1][$i]) !== '') { // Non-empty
171                    // Apply cell format if specified:
172                    if (($fmt = $o_column_formats[$col]) != '')
173                        $cell = sprintf($fmt, $cell);
174                    if (($this_len = strlen($cell)) > $len)
175                        $len = $this_len;
176                }
177            }
178            // Assign column width, and accumulate the total:
179            $o_width += $col_width[$col++] = $len * $char_width + $pad2;
180        }
181    } else { // Table width, and optionally column width factors, are supplied.
182        // Calculate the width of each column.
183        $col_width_scale = $o_width / array_sum($o_column_widths);
184        for ($col = 0; $col < $n_cols; $col++)
185            $col_width[$col] = $o_column_widths[$col] * $col_width_scale;
186    }
187
188    // Calculate the column start positions within the table:
189    $col_start[0] = 0;
190    for ($i = 1; $i < $n_cols; $i++)
191        $col_start[$i] = $col_start[$i-1] + $col_width[$i-1];
192
193    // Draw the table grid (without outer border)
194    $x2 = $x + $o_width - 1;
195    for ($row = 1; $row < $n_rows; $row++) {
196        $y0 = $y + $row_height * $row; // Avoid accumulating errors.
197        imageline($img, $x, $y0, $x2, $y0, $color);
198    }
199    $y2 = $y + $o_height - 1;
200    for ($col = 1; $col < $n_cols; $col++) {
201        $x0 = $x + $col_start[$col];
202        imageline($img, $x0, $y, $x0, $y2, $color);
203    }
204
205    // Draw the header row, then the data rows
206    for ($row = 0; $row < $n_rows; $row++) {
207
208        // Vertically center the cell contents within the cell:
209        $y0 = $y + $row_height * ($row + 0.5) - $char_height / 2;
210
211        if ($row == 0) $cells = $o_headers; // Header row
212        else $cells = $o_data[$row - 1]; // -1 accounts for header row.
213
214        $col = 0; // Index to unskipped columns
215        for ($i = 0; $i < $n_data_cols; $i++) { // Index to all columns
216
217            if (is_null($o_headers[$i])) continue; // NULL header => skip column
218
219            if (($cell = $cells[$i]) !== '') { // Empty cell?
220                if ($row == 0) {
221                    $alg = 'C'; // Header row forces center alignment
222                } else {
223                    // Apply cell format if specified:
224                    if (($fmt = $o_column_formats[$col]) != '')
225                        $cell = sprintf($fmt, $cell);
226                    // Get cell alignment:
227                    $alg = $o_column_alignments[$col];
228                }
229
230                // Calculate upper left position for this cell's text:
231                if (empty($alg)) // Default alignment: numbers right, else left.
232                    $alg = is_numeric($cell) ? 'R' : 'L';
233                $x0 = $x + $col_start[$col];
234                if ($alg == 'R') {
235                    $x0 += $col_width[$col] - strlen($cell) * $char_width - $o_cellpadding;
236                } elseif ($alg == 'C') {
237                    $x0 += ($col_width[$col] - strlen($cell) * $char_width) / 2;
238                } else { // Default, assume L
239                    $x0 += $o_cellpadding;
240                }
241                imagestring($img, $o_font, $x0, $y0, $cell, $color);
242            }
243            $col++;
244        }
245    }
246    return TRUE;
247}
248