<?php
-/* $Id: phplot.php,v 1.216 2011/01/16 01:19:55 lbayuk Exp $ */
+/* $Id: phplot.php,v 1.224 2011/05/27 18:05:16 lbayuk Exp $ */
 /*
- * PHPLOT Version 5.3.1
+ * PHPLOT Version 5.4.0
  *
  * A PHP class for creating scientific and business charts
  * Visit http://sourceforge.net/projects/phplot/
 
 class PHPlot
 {
+    const version = '5.4.0';
+
     /* Declare class variables which are initialized to static values. Many more class variables
      * are used, defined as needed, but are unset by default.
      * All these are declared as public. While it is tempting to make them private or protected, this
 
 // Legend
     public $legend = '';                       // An array with legend titles
-    // These variables are unset to take default values:
-    // public $legend_x_pos;                   // User-specified upper left coordinates of legend box
-    // public $legend_y_pos;
-    // public $legend_xy_world;                // If set, legend_x/y_pos are world coords, else pixel coords
-    // public $legend_text_align;              // left or right, Unset means right
-    // public $legend_colorbox_align;          // left, right, or none; Unset means same as text_align
+    // Other legend_* variables are set as needed, unset for default values.
 
 //Ticks
     public $x_tick_length = 5;                 // tick length in pixels for upper/lower axis
     }
 
     /*
-     * Sets the colors for the data, with optional default alpha value (for PHPlot_truecolor only)
+     * Sets the colors for the data, with optional default alpha value
      * Cases are:
      *    SetDataColors(array(...))  : Use the supplied array as the color map.
      *    SetDataColors(colorname)   : Use an array of just colorname as the color map.
         return TRUE;
     }
 
-    /*
-     * Set text to display in the graph's legend.
-     *   $which_leg : Array of strings for the complete legend, or a single string
-     *                to be appended to the legend.
-     *                Or NULL (or an empty array) to cancel the legend.
-     */
-    function SetLegend($which_leg)
-    {
-        if (is_array($which_leg)) {           // use array (or cancel, if empty array)
-            $this->legend = $which_leg;
-        } elseif (!is_null($which_leg)) {     // append string
-            $this->legend[] = $which_leg;
-        } else {
-            $this->legend = '';  // Reinitialize to empty, meaning no legend.
-        }
-        return TRUE;
-    }
-
-    /*
-     * Specifies the position of the legend's upper/leftmost corner,
-     * in pixel (device) coordinates.
-     * Both X and Y must be provided, or both omitted (or use NULL) to restore auto-positioning.
-     */
-    function SetLegendPixels($which_x=NULL, $which_y=NULL)
-    {
-        $this->legend_x_pos = $which_x;
-        $this->legend_y_pos = $which_y;
-        // Make sure this is unset, meaning we have pixel coords:
-        unset($this->legend_xy_world);
-
-        return TRUE;
-    }
-
-    /*
-     * Specifies the position of the legend's upper/leftmost corner,
-     * in world (data space) coordinates.
-     */
-    function SetLegendWorld($which_x, $which_y)
-    {
-        // Since conversion from world to pixel coordinates is not yet available, just
-        // remember the coordinates and set a flag to indicate conversion is needed.
-        $this->legend_x_pos = $which_x;
-        $this->legend_y_pos = $which_y;
-        $this->legend_xy_world = TRUE;
-
-        return TRUE;
-    }
-
-    /*
-     * Set legend text alignment, color box alignment, and style options.
-     *   $text_align : Alignment of the text, 'left' or 'right'.
-     *   $colorbox_align : Alignment of the color boxes, 'left', 'right', 'none', or missing/empty.
-     *       If missing or empty, the same alignment as $text_align is used. Color box is positioned first.
-     *   $style : reserved for future use.
-     */
-    function SetLegendStyle($text_align, $colorbox_align = '', $style = '')
-    {
-        $this->legend_text_align = $this->CheckOption($text_align, 'left, right', __FUNCTION__);
-        if (empty($colorbox_align))
-            $this->legend_colorbox_align = $this->legend_text_align;
-        else
-            $this->legend_colorbox_align = $this->CheckOption($colorbox_align, 'left, right, none',
-                                                              __FUNCTION__);
-        return ((boolean)$this->legend_text_align && (boolean)$this->legend_colorbox_align);
-    }
-
     /*
      * Set border for the plot area.
      * Accepted values are: left, right, top, bottom, sides, none, full or an array of those.
             $data_min = $this->plot_min_x;
             $skip_lo = $this->skip_left_tick;
             $skip_hi = $this->skip_right_tick;
+            $anchor = &$this->x_tick_anchor; // Use reference because this might not be set
         } elseif ($which == 'y') {
             $num_ticks = $this->num_y_ticks;
             $tick_inc = $this->y_tick_inc;
             $data_min = $this->plot_min_y;
             $skip_lo = $this->skip_bottom_tick;
             $skip_hi = $this->skip_top_tick;
+            $anchor = &$this->y_tick_anchor; // Use reference because this might not be set
         } else {
             return $this->PrintError("CalcTicks: Invalid usage ($which)");
         }
         $tick_start = (double)$data_min;
         $tick_end = (double)$data_max + ($data_max - $data_min) / 10000.0;
 
+        // If a tick anchor was given, adjust the start of the range so the anchor falls
+        // at an exact tick mark (or would, if it was within range).
+        if (isset($anchor)) {
+            $tick_start = $anchor - $tick_step * floor(($anchor - $tick_start) / $tick_step);
+        }
+
+        // Lastly, adjust for option to skip left/bottom or right/top tick marks:
         if ($skip_lo)
             $tick_start += $tick_step;
-
         if ($skip_hi)
             $tick_end -= $tick_step;
 
         return TRUE;
     }
 
+    /*
+     * Set an anchor point for X tick marks. There will be an X tick mark at
+     * this exact value (if the data range were extended to include it).
+     */
+    function SetXTickAnchor($xta = NULL)
+    {
+        $this->x_tick_anchor = $xta;
+        return TRUE;
+    }
+
+    /*
+     * Set an anchor point for Y tick marks. There will be a Y tick mark at
+     * this exact value (if the data range were extended to include it).
+     */
+    function SetYTickAnchor($yta = NULL)
+    {
+        $this->y_tick_anchor = $yta;
+        return TRUE;
+    }
+
 /////////////////////////////////////////////
 ////////////////////          GENERIC DRAWING
 /////////////////////////////////////////////
         return TRUE;
     }
 
+/////////////////////////////////////////////
+///////////////                        LEGEND
+/////////////////////////////////////////////
+
     /*
-     * Draws the graph legend
-     * This is called by DrawGraph only if $this->legend is not empty.
-     * Base code submitted by Marlin Viss
+     * Set text to display in the graph's legend.
+     *   $which_leg : Array of strings for the complete legend, or a single string
+     *                to be appended to the legend.
+     *                Or NULL (or an empty array) to cancel the legend.
      */
-    protected function DrawLegend()
+    function SetLegend($which_leg)
+    {
+        if (is_array($which_leg)) {           // use array (or cancel, if empty array)
+            $this->legend = $which_leg;
+        } elseif (!is_null($which_leg)) {     // append string
+            $this->legend[] = $which_leg;
+        } else {
+            $this->legend = '';  // Reinitialize to empty, meaning no legend.
+        }
+        return TRUE;
+    }
+
+    /*
+     * Specifies the position of the legend's upper/leftmost corner, in pixel (device) coordinates.
+     * Both X and Y must be provided, or both omitted (or use NULL) to restore auto-positioning.
+     */
+    function SetLegendPixels($which_x=NULL, $which_y=NULL)
     {
-        $font = &$this->fonts['legend'];
+        return $this->SetLegendPosition(0, 0, 'image', 0, 0, $which_x, $which_y);
+    }
+
+    /*
+     * Specifies the position of the legend's upper/leftmost corner, in world (data space) coordinates.
+     */
+    function SetLegendWorld($which_x, $which_y)
+    {
+        return $this->SetLegendPosition(0, 0, 'world', $which_x, $which_y);
+    }
+
+    /*
+     * Specifies the position of the legend. This includes SetLegendWorld(), SetLegendPixels(), and
+     * additional choices using relative coordinates, with optional pixel offset.
+     *   $x, $y : Relative coordinates of a point on the legend box. (See below)
+     *   $relative_to : What to position the legend relative to: 'image', 'plot', 'world', or 'title'.
+     *   $x_base, $y_base : Base point for positioning.
+     *      If $relative_to is 'world', then this is a world coordinate position.
+     *      Otherwise, this is a relative coordinate position on the $relative_to element.
+     *   $x_offset, $y_offset : Additional legend box offset in device coordinates (pixels).
+     *  The legend is positioned so that point ($x,$y) is at ($x_base, $y_base).
+     *  'Relative coordinates' means: (0,0) is the upper left corner, and (1,1) is the lower right corner
+     *  of the element (legend, image, plot, or title area), regardless of its size. These are floating
+     *  point values, each usually in the range [0,1], but they can be negative or greater than 1.
+     *  If any of x, y, x_offset, or y_offset are NULL, default legend positioning is restored.
+     */
+    function SetLegendPosition($x, $y, $relative_to, $x_base, $y_base, $x_offset = 0, $y_offset = 0)
+    {
+        // Special case: NULL means restore the default positioning.
+        if (!isset($x, $y, $x_offset, $y_offset)) {
+            unset($this->legend_pos);
+        } else {
+            $mode = $this->CheckOption($relative_to, 'image, plot, title, world', __FUNCTION__);
+            if (empty($mode))
+                return FALSE;
+            // Save all values for use by GetLegendPosition()
+            $this->legend_pos = compact('x', 'y', 'mode', 'x_base', 'y_base', 'x_offset', 'y_offset');
+        }
+        return TRUE;
+    }
+
+    /*
+     * Set legend text alignment, color box alignment, and style options.
+     *   $text_align : Alignment of the text, 'left' or 'right'.
+     *   $colorbox_align : Alignment of the color boxes, 'left', 'right', 'none', or missing/empty.
+     *       If missing or empty, the same alignment as $text_align is used. Color box is positioned first.
+     */
+    function SetLegendStyle($text_align, $colorbox_align = '')
+    {
+        $this->legend_text_align = $this->CheckOption($text_align, 'left, right', __FUNCTION__);
+        if (empty($colorbox_align))
+            $this->legend_colorbox_align = $this->legend_text_align;
+        else
+            $this->legend_colorbox_align = $this->CheckOption($colorbox_align, 'left, right, none',
+                                                              __FUNCTION__);
+        return ((boolean)$this->legend_text_align && (boolean)$this->legend_colorbox_align);
+    }
+
+    /*
+     * Use color boxes or point shapes (for points and linepoints plots only) in the legend.
+     *   $use_shapes : True to use point shapes, false to use color boxes.
+     */
+    function SetLegendUseShapes($use_shapes)
+    {
+        $this->legend_use_shapes = (bool)$use_shapes;
+        return TRUE;
+    }
+
+    /*
+     * Get legend sizing parameters.
+     * This is used internally by DrawLegend(), and also by the public GetLegendSize().
+     * It returns information based on any SetLegend*() calls already made. It does not use
+     * legend position or data scaling, so it can be called before data scaling is set up.
+     * Returns an associative array with these entries describing legend sizing:
+     *    'width', 'height' : Overall legend box size in pixels.
+     *    'char_w', 'char_h' : Width and height of 'E' in legend text font. (Used to size color boxes)
+     *    'v_margin' : Inside margin for legend
+     *    'text_align', 'colorbox_align' : Same as the class variables, with default applied.
+     *    'draw_colorbox' : True if color boxes will be drawn.
+     *    'dot_height' : Height of color boxes (even if not drawn), for line spacing.
+     *    'colorbox_width' : Width of color boxes.
+     */
+    protected function GetLegendSizeParams()
+    {
+        $font = &$this->fonts['legend']; // Shortcut to font info array
 
         // Find maximum legend label line width.
         $max_width = 0;
             if ($width > $max_width) $max_width = $width;
         }
 
-        // Use the font parameters to size the color boxes:
+        // Font parameters are used to size the color boxes:
         $char_w = $font['width'];
         $char_h = $font['height'];
         $line_spacing = $this->GetLineSpacing($font);
 
-        // Normalize text alignment and colorbox alignment variables:
+        // Apply defaults to text alignment and colorbox alignment variables:
         $text_align = isset($this->legend_text_align) ? $this->legend_text_align : 'right';
         $colorbox_align = isset($this->legend_colorbox_align) ? $this->legend_colorbox_align : 'right';
+        $draw_colorbox = ($colorbox_align != 'none');
 
         // Sizing parameters:
-        $v_margin = $char_h/2;                   // Between vertical borders and labels
-        $dot_height = $char_h + $line_spacing;   // Height of the small colored boxes
-        // Color boxes are $char_w wide, but can be adjusted using legend_colorbox_width:
-        $colorbox_width = $char_w;
+        $v_margin = $char_h / 2;                 // Between vertical borders and labels
+        $dot_height = $char_h + $line_spacing;   // Height of the color boxes (even if not drawn)
+        $colorbox_width = $char_w;               // Base color box width
         if (isset($this->legend_colorbox_width))
-            $colorbox_width *= $this->legend_colorbox_width;
+            $colorbox_width *= $this->legend_colorbox_width; // Adjustment to color box width
 
-        // Overall legend box width e.g.: | space colorbox space text space |
-        // where each space adds $char_w, and colorbox adds $char_w * its width adjustment.
-        if (($draw_colorbox = ($colorbox_align != 'none'))) {
+        // Calculate overall legend box width and height.
+        // Width is e.g.: "| space colorbox space text space |" where each space adds $char_w,
+        // and colorbox (if drawn) adds $char_w * its width adjustment.
+        if ($draw_colorbox) {
             $width = $max_width + 3 * $char_w + $colorbox_width;
         } else {
             $width = $max_width + 2 * $char_w;
         }
+        $height = $dot_height * count($this->legend) + 2 * $v_margin;
 
-        //////// Calculate box position
-        // User-defined position specified?
-        if ( !isset($this->legend_x_pos) || !isset($this->legend_y_pos)) {
-            // No, use default
-            $box_start_x = $this->plot_area[2] - $width - $this->safe_margin;
-            $box_start_y = $this->plot_area[1] + $this->safe_margin;
-        } elseif (isset($this->legend_xy_world)) {
-            // User-defined position in world-coordinates (See SetLegendWorld).
-            $box_start_x = $this->xtr($this->legend_x_pos);
-            $box_start_y = $this->ytr($this->legend_y_pos);
-        } else {
-            // User-defined position in pixel coordinates.
-            $box_start_x = $this->legend_x_pos;
-            $box_start_y = $this->legend_y_pos;
+        return compact('width', 'height', 'char_w', 'char_h', 'v_margin',
+              'text_align', 'colorbox_align', 'draw_colorbox', 'dot_height', 'colorbox_width');
+    }
+
+    /*
+     * Get legend box size. This can be used to adjust the plot margins, for example.
+     * Returns: Array of ($width, $height) of the legend box in pixels.
+     */
+    function GetLegendSize()
+    {
+        $params = $this->GetLegendSizeParams();
+        return array($params['width'], $params['height']);
+    }
+
+    /*
+     * Get legend location in device coordinates. This is a helper for DrawLegend, and is only
+     * called if there is a legend. See SetLegendWorld(), SetLegendPixels(), SetLegendPosition().
+     *   $width, $height : Width and height of the legend box.
+     * Returns: coordinates of the upper left corner of the legend box as an array ($x, $y)
+     */
+    protected function GetLegendPosition($width, $height)
+    {
+        // Extract variables set by SetLegend*(): $mode, $x, $y, $x_base, $y_base, $x_offset, $y_offset
+        if (isset($this->legend_pos['mode']))
+            extract($this->legend_pos);
+        else
+            $mode = ''; // Default legend position mode.
+
+        switch ($mode) {
+
+        case 'plot': // SetLegendPosition with mode='plot', relative coordinates over plot area.
+            return array((int)($x_base * $this->plot_area_width - $x * $width)
+                          + $this->plot_area[0] + $x_offset,
+                         (int)($y_base * $this->plot_area_height - $y * $height)
+                          + $this->plot_area[1] + $y_offset);
+
+        case 'world': // User-defined position in world-coordinates (SetLegendWorld), using x_base, y_base
+            return array($this->xtr($x_base) + $x_offset - (int)($x * $width),
+                         $this->ytr($y_base) + $y_offset - (int)($y * $height));
+
+        case 'image': // SetLegendPosition with mode='image', relative coordinates over image area.
+                      // SetLegendPixels() uses this too, with x=y=0.
+            return array((int)($x_base * $this->image_width - $x * $width) + $x_offset,
+                         (int)($y_base * $this->image_height - $y * $height) + $y_offset);
+
+        case 'title': // SetLegendPosition with mode='title', relative to main title.
+            // Recalculate main title position/size, since CalcMargins does not save it. See DrawTitle()
+            list($title_width, $title_height) = $this->SizeText($this->fonts['title'], 0, $this->title_txt);
+            $title_x = (int)(($this->image_width - $title_width) / 2);
+            return array((int)($x_base * $title_width - $x * $width) + $title_x + $x_offset,
+                         (int)($y_base * $title_height - $y * $height) + $this->title_offset + $y_offset);
+
+        default: // If mode is unset (or invalid), use default position.
+            return array ($this->plot_area[2] - $width - $this->safe_margin,
+                          $this->plot_area[1] + $this->safe_margin);
         }
+    }
+
+    /*
+     * Draws the graph legend
+     * This is called by DrawGraph only if $this->legend is not empty.
+     * Base code submitted by Marlin Viss
+     */
+    protected function DrawLegend()
+    {
+        $font = &$this->fonts['legend']; // Shortcut to font info array
+
+        // Calculate legend box sizing parameters:
+        // See GetLegendSizeParams() to see what variables are set by this.
+        extract($this->GetLegendSizeParams());
 
-        // Lower right corner
-        $box_end_y = $box_start_y + $dot_height*(count($this->legend)) + 2*$v_margin;
+        // Get legend box position:
+        list($box_start_x, $box_start_y) = $this->GetLegendPosition($width, $height);
+        $box_end_y = $box_start_y + $height;
         $box_end_x = $box_start_x + $width;
 
         // Draw outer box
                 $x_pos = $box_start_x + $char_w;
             else
                 $x_pos = $box_end_x - $char_w;
+            $dot_left_x = 0; // Not used directly if color boxes/shapes are off, but referenced below.
         } elseif ($colorbox_align == 'left') {
             $dot_left_x = $box_start_x + $char_w;
             $dot_right_x = $dot_left_x + $colorbox_width;
                 $x_pos = $dot_left_x - $char_w;
         }
 
-        // Calculate starting position of first text line.  The bottom of each color box
-        // lines up with the bottom (baseline) of its text line.
+        // $y_pos is the bottom of each color box. $yc is the vertical center of the color box or
+        // the point shape (if drawn). The text is centered vertically on $yc.
         $y_pos = $box_start_y + $v_margin + $dot_height;
+        $yc = (int)($y_pos - $dot_height / 2);
+        $xc = (int)($dot_left_x + $colorbox_width / 2);   // Horizontal center for point shape if drawn
+        $shape_index = 0;  // Shape number index, if drawing point shapes
+
+        // Option to use point shapes rather than solid boxes. Disallow this if the shapes array
+        // has not been initialized (see CheckPointParams). Only works with 'points' or 'linepoints' plots.
+        $use_shapes = !empty($this->legend_use_shapes) && !empty($this->point_counts);
 
         foreach ($this->legend as $leg) {
             // Draw text with requested alignment:
-            $this->DrawText($font, 0, $x_pos, $y_pos, $this->ndx_text_color, $leg, $text_align, 'bottom');
+            $this->DrawText($font, 0, $x_pos, $yc, $this->ndx_text_color, $leg, $text_align, 'center');
             if ($draw_colorbox) {
-                // Draw a box in the data color
                 $y1 = $y_pos - $dot_height + 1;
                 $y2 = $y_pos - 1;
-                ImageFilledRectangle($this->img, $dot_left_x, $y1, $dot_right_x, $y2,
-                                     $this->ndx_data_colors[$color_index]);
-                // Draw a rectangle around the box
-                ImageRectangle($this->img, $dot_left_x, $y1, $dot_right_x, $y2,
-                               $this->ndx_text_color);
+                if ($use_shapes) {
+                    // Draw a point shape in the data color
+                    // If plot area background is on, use that as the shape background:
+                    if ($this->draw_plot_area_background) {
+                        ImageFilledRectangle($this->img, $dot_left_x, $y1, $dot_right_x, $y2,
+                                             $this->ndx_plot_bg_color);
+                    }
+                    // Draw the shape. DrawShape() takes shape_index modulo number of defined shapes.
+                    $this->DrawShape($xc, $yc, $shape_index++, $this->ndx_data_colors[$color_index]);
+                } else {
+                    // Draw color boxes:
+                    ImageFilledRectangle($this->img, $dot_left_x, $y1, $dot_right_x, $y2,
+                                         $this->ndx_data_colors[$color_index]);
+                   // Draw a rectangle around the box
+                   ImageRectangle($this->img, $dot_left_x, $y1, $dot_right_x, $y2, $this->ndx_text_color);
+                }
             }
             $y_pos += $dot_height;
-
-            $color_index++;
-            if ($color_index > $max_color_index)
+            $yc += $dot_height;
+            if (++$color_index > $max_color_index)
                 $color_index = 0;
         }
         return TRUE;
     }
 
     /*
-     * Draws a styled dot. Uses world coordinates.
+     * Draw a shape (dot, point). This is the bottom half of DrawDot, and is also
+     * used by legend drawing. Unlike DrawDot this takes device coordinates.
      * The list of supported shapes can also be found in SetPointShapes().
-     * All shapes are drawn using a 3x3 grid, centered on the data point.
-     * The center is (x_mid, y_mid) and the corners are (x1, y1) and (x2, y2).
-     *   $record is the 0-based index that selects the shape and size.
+     *   $x, $y - Device coordinates of the center of the shape
+     *   $record - Index into point_shapes[] and point_sizes[]. This is taken modulo the array sizes.
+     *   $color - Shape color to use.
      */
-    protected function DrawDot($x_world, $y_world, $record, $color)
+    protected function DrawShape($x, $y, $record, $color)
     {
         $index = $record % $this->point_counts;
         $point_size = $this->point_sizes[$index];
-
         $half_point = (int)($point_size / 2);
 
-        $x_mid = $this->xtr($x_world);
-        $y_mid = $this->ytr($y_world);
-
-        $x1 = $x_mid - $half_point;
-        $x2 = $x_mid + $half_point;
-        $y1 = $y_mid - $half_point;
-        $y2 = $y_mid + $half_point;
+        $x1 = $x - $half_point;
+        $x2 = $x + $half_point;
+        $y1 = $y - $half_point;
+        $y2 = $y + $half_point;
 
         switch ($this->point_shapes[$index]) {
         case 'halfline':
-            ImageLine($this->img, $x1, $y_mid, $x_mid, $y_mid, $color);
+            ImageLine($this->img, $x1, $y, $x, $y, $color);
             break;
         case 'line':
-            ImageLine($this->img, $x1, $y_mid, $x2, $y_mid, $color);
+            ImageLine($this->img, $x1, $y, $x2, $y, $color);
             break;
         case 'plus':
-            ImageLine($this->img, $x1, $y_mid, $x2, $y_mid, $color);
-            ImageLine($this->img, $x_mid, $y1, $x_mid, $y2, $color);
+            ImageLine($this->img, $x1, $y, $x2, $y, $color);
+            ImageLine($this->img, $x, $y1, $x, $y2, $color);
             break;
         case 'cross':
             ImageLine($this->img, $x1, $y1, $x2, $y2, $color);
             ImageLine($this->img, $x1, $y2, $x2, $y1, $color);
             break;
         case 'circle':
-            ImageArc($this->img, $x_mid, $y_mid, $point_size, $point_size, 0, 360, $color);
+            ImageArc($this->img, $x, $y, $point_size, $point_size, 0, 360, $color);
             break;
         case 'dot':
-            ImageFilledEllipse($this->img, $x_mid, $y_mid, $point_size, $point_size, $color);
+            ImageFilledEllipse($this->img, $x, $y, $point_size, $point_size, $color);
             break;
         case 'diamond':
-            $arrpoints = array( $x1, $y_mid, $x_mid, $y1, $x2, $y_mid, $x_mid, $y2);
+            $arrpoints = array($x1, $y, $x, $y1, $x2, $y, $x, $y2);
             ImageFilledPolygon($this->img, $arrpoints, 4, $color);
             break;
         case 'triangle':
-            $arrpoints = array( $x1, $y_mid, $x2, $y_mid, $x_mid, $y2);
+            $arrpoints = array($x1, $y, $x2, $y, $x, $y2);
             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
             break;
         case 'trianglemid':
-            $arrpoints = array( $x1, $y1, $x2, $y1, $x_mid, $y_mid);
+            $arrpoints = array($x1, $y1, $x2, $y1, $x, $y);
             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
             break;
         case 'yield':
-            $arrpoints = array( $x1, $y1, $x2, $y1, $x_mid, $y2);
+            $arrpoints = array($x1, $y1, $x2, $y1, $x, $y2);
             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
             break;
         case 'delta':
-            $arrpoints = array( $x1, $y2, $x2, $y2, $x_mid, $y1);
+            $arrpoints = array($x1, $y2, $x2, $y2, $x, $y1);
             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
             break;
         case 'star':
-            ImageLine($this->img, $x1, $y_mid, $x2, $y_mid, $color);
-            ImageLine($this->img, $x_mid, $y1, $x_mid, $y2, $color);
+            ImageLine($this->img, $x1, $y, $x2, $y, $color);
+            ImageLine($this->img, $x, $y1, $x, $y2, $color);
             ImageLine($this->img, $x1, $y1, $x2, $y2, $color);
             ImageLine($this->img, $x1, $y2, $x2, $y1, $color);
             break;
         case 'hourglass':
-            $arrpoints = array( $x1, $y1, $x2, $y1, $x1, $y2, $x2, $y2);
+            $arrpoints = array($x1, $y1, $x2, $y1, $x1, $y2, $x2, $y2);
             ImageFilledPolygon($this->img, $arrpoints, 4, $color);
             break;
         case 'bowtie':
-            $arrpoints = array( $x1, $y1, $x1, $y2, $x2, $y1, $x2, $y2);
+            $arrpoints = array($x1, $y1, $x1, $y2, $x2, $y1, $x2, $y2);
             ImageFilledPolygon($this->img, $arrpoints, 4, $color);
             break;
         case 'target':
-            ImageFilledRectangle($this->img, $x1, $y1, $x_mid, $y_mid, $color);
-            ImageFilledRectangle($this->img, $x_mid, $y_mid, $x2, $y2, $color);
+            ImageFilledRectangle($this->img, $x1, $y1, $x, $y, $color);
+            ImageFilledRectangle($this->img, $x, $y, $x2, $y2, $color);
             ImageRectangle($this->img, $x1, $y1, $x2, $y2, $color);
             break;
         case 'box':
             ImageRectangle($this->img, $x1, $y1, $x2, $y2, $color);
             break;
         case 'home': /* As in: "home plate" (baseball), also looks sort of like a house. */
-            $arrpoints = array( $x1, $y2, $x2, $y2, $x2, $y_mid, $x_mid, $y1, $x1, $y_mid);
+            $arrpoints = array($x1, $y2, $x2, $y2, $x2, $y, $x, $y1, $x1, $y);
             ImageFilledPolygon($this->img, $arrpoints, 5, $color);
             break;
         case 'up':
-            ImagePolygon($this->img, array($x_mid, $y1, $x2, $y2, $x1, $y2), 3, $color);
+            ImagePolygon($this->img, array($x, $y1, $x2, $y2, $x1, $y2), 3, $color);
             break;
         case 'down':
-            ImagePolygon($this->img, array($x_mid, $y2, $x1, $y1, $x2, $y1), 3, $color);
+            ImagePolygon($this->img, array($x, $y2, $x1, $y1, $x2, $y1), 3, $color);
             break;
         case 'none': /* Special case, no point shape here */
             break;
         return TRUE;
     }
 
+    /*
+     * Draws a styled dot. Uses world coordinates.
+     * Note: DrawShape() does all the work.
+     */
+    protected function DrawDot($x_world, $y_world, $record, $color)
+    {
+        return $this->DrawShape($this->xtr($x_world), $this->ytr($y_world), $record, $color);
+    }
+
     /*
      * Draw a bar (or segment of a bar), with optional shading or border.
      * This is used by the bar and stackedbar plots, vertical and horizontal.
             if ($this->x_data_label_pos != 'none')          // Draw X Data labels?
                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels);
 
-            // Lower left and lower right X of the bars in this stack:
-            $x1 = $x_now_pixels - $x_first_bar;
-            $x2 = $x1 + $this->actual_bar_width;
-
-            // Draw the bar segments in this stack.
-            $wy1 = 0;                       // World coordinates Y1, current sum of values
-            $wy2 = $this->x_axis_position;  // World coordinates Y2, last drawn value
-            $first = TRUE;
+            // Determine bar direction based on 1st non-zero value. Note the bar direction is
+            // based on zero, not the axis value.
+            $n_recs = $this->num_recs[$row];
+            $upward = TRUE; // Initialize this for the case of all segments = 0
+            for ($i = $record; $i < $n_recs; $i++) {
+                if (is_numeric($this_y = $this->data[$row][$i]) && $this_y != 0) {
+                    $upward = ($this_y > 0);
+                    break;
+                }
+            }
 
-            for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
+            $x1 = $x_now_pixels - $x_first_bar;  // Left X of bars in this stack
+            $x2 = $x1 + $this->actual_bar_width; // Right X of bars in this stack
+            $wy1 = 0;                            // World coordinates Y1, current sum of values
+            $wy2 = $this->x_axis_position;       // World coordinates Y2, last drawn value
 
-                // Skip missing Y values, and ignore Y=0 values.
-                if (is_numeric($this->data[$row][$record])
-                    && ($this_y = $this->data[$row][$record]) != 0) {
+            // Draw bar segments and labels in this stack.
+            $first = TRUE;
+            for ($idx = 0; $record < $n_recs; $record++, $idx++) {
 
-                    // First non-zero value sets the direction, $upward. Note this compares to 0,
-                    // not the axis position. Segments are based at 0 but clip to the axis.
-                    if ($first)
-                        $upward = ($this_y > 0);
+                // Skip missing Y values. Process Y=0 values due to special case of moved axis.
+                if (is_numeric($this_y = $this->data[$row][$record])) {
 
                     $wy1 += $this_y;    // Keep the running total for this bar stack
 
                         }
                         // Mark the new end of the bar, conditional on segment height > 0.
                         $wy2 = $wy1;
+                        $first = FALSE;
                     }
-                    $first = FALSE;
                 }
             }   // end for
 
             if ($this->y_data_label_pos != 'none')          // Draw Y Data labels?
                 $this->DrawYDataLabel($this->data[$row][0], $y_now_pixels);
 
+            // Determine bar direction based on 1st non-zero value. Note the bar direction is
+            // based on zero, not the axis value.
+            $n_recs = $this->num_recs[$row];
+            $rightward = TRUE; // Initialize this for the case of all segments = 0
+            for ($i = $record; $i < $n_recs; $i++) {
+                if (is_numeric($this_x = $this->data[$row][$i]) && $this_x != 0) {
+                    $rightward = ($this_x > 0);
+                    break;
+                }
+            }
+
             // Lower left and upper left Y of the bars in this stack:
-            $y1 = $y_now_pixels + $y_first_bar;
-            $y2 = $y1 - $this->actual_bar_width;
+            $y1 = $y_now_pixels + $y_first_bar;  // Lower Y of bars in this stack
+            $y2 = $y1 - $this->actual_bar_width; // Upper Y of bars in this stack
+            $wx1 = 0;                            // World coordinates X1, current sum of values
+            $wx2 = $this->y_axis_position;       // World coordinates X2, last drawn value
 
-            // Draw the bar segments in this stack:
-            $wx1 = 0;                       // World coordinates X1, current sum of values
-            $wx2 = $this->y_axis_position;  // World coordinates X2, last drawn value
+            // Draw bar segments and labels in this stack.
             $first = TRUE;
-
             for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
 
-                // Skip missing X values, and ignore X<0 values.
-                if (is_numeric($this->data[$row][$record])
-                    && ($this_x = $this->data[$row][$record]) != 0) {
-
-                    // First non-zero value sets the direction, $rightward. Note this compares to 0,
-                    // not the axis position. Segments are based at 0 but clip to the axis.
-                    if ($first)
-                        $rightward = ($this_x > 0);
+                // Skip missing X values. Process Y=0 values due to special case of moved axis.
+                if (is_numeric($this_x = $this->data[$row][$record])) {
 
                     $wx1 += $this_x;  // Keep the running total for this bar stack
 
                         }
                         // Mark the new end of the bar, conditional on segment width > 0.
                         $wx2 = $wx1;
+                        $first = FALSE;
                     }
-                    $first = FALSE;
                 }
             }   // end for