DBD::Chart::Plot - Graph/chart Plotting engine for DBD::Chart


DBD-Chart documentation Contained in the DBD-Chart distribution.

Index


Code Index:

NAME

Top

DBD::Chart::Plot - Graph/chart Plotting engine for DBD::Chart

SYNOPSIS

Top

    use DBD::Chart::Plot;

    my $img = DBD::Chart::Plot->new();
    my $anotherImg = DBD::Chart::Plot->new($image_width, $image_height);

    $img->setPoints(\@xdataset, \@ydataset, 'blue line nopoints');

    $img->setOptions (
        horizMargin => 75,
        vertMargin => 100,
        title => 'My Graph Title',
        xAxisLabel => 'my X label',
        yAxisLabel => 'my Y label' );

    print $img->plot;

DESCRIPTION

Top

DBD::Chart::Plot creates images of various types of graphs for 2 or 3 dimensional data. Unlike GD::Graph, the input data sets do not need to be uniformly distributed in the domain (X-axis), and may be either numeric, temporal, or symbolic.

DBD::Chart::Plot supports the following:

- multiple data set plots
- line graphs, areagraphs, scatter graphs, linegraphs w/ points, candlestick graphs, barcharts (2-D, 3-D, and 3-axis), histograms, piecharts, box & whisker charts (aka boxcharts), and Gantt charts
- optional iconic barcharts or datapoints
- a wide selection of colors, and point shapes
- optional horizontal and/or vertical gridlines
- optional legend
- auto-sizing of axes based in input dataset ranges
- optional symbolic and temproal (i.e., non-numeric) domain values
- automatic sorting of numeric and temporal input datasets to assure proper order of plotting
- optional X, Y, and Z axis labels
- optional X and/or Y logarithmic scaling
- optional title
- optional adjustment of horizontal and vertical margins
- optional HTML or Perl imagemap generation
- composite images from multiple graphs
- user programmable colors

PREREQUISITES

Top

GD.pm module minimum version 1.26 (available on CPAN)

GD.pm requires additional libraries:

libgd
libpng
zlib

USAGE

Top

Create an image object: new()

    use DBD::Chart::Plot;

    my $img = DBD::Chart::Plot->new;
    my $img = DBD::Chart::Plot->new ( $image_width, $image_height );
    my $img = DBD::Chart::Plot->new ( $image_width, $image_height, \%colormap );
    my $anotherImg = new DBD::Chart::Plot;

Creates an empty image. If image size is not specified, the default is 400 x 300 pixels.

Graph-wide options: setOptions()



    $img->setOptions (_title => 'My Graph Title',
        xAxisLabel => 'my X label',
        yAxisLabel => 'my Y label',
        xLog => 0,
        yLog => 0,
        horizMargin => $numHorPixels,
        vertMargin => $numvertPixels,
        horizGrid => 1,
        vertGrid => 1,
        showValues => 1,
        legend => \@plotnames,
        genMap => 'a_valid_HTML_anchor_name',
        mapURL => 'http://some.website.com/cgi-bin/cgi.pl',
        icon => [ 'redstar.png', 'bluestar.png' ]
        symDomain => 0
     );

As many (or few) of the options may be specified as desired.

width, height

The width and height of the image in pixels. Default is 400 and 300, respectively.

genMap, mapType, mapURL, mapScript

Control generation of imagemaps. When genMap is set to a legal HTML anchor name, an image map of the specified type is created for the image. The default type is 'HTML' if no mapType is specified. Legal types are 'HTML' and 'PERL'.

If mapType is 'PERL', then Perl script compatible text is generated representing an array ref of hashrefs containing the following attributes:

plotnum => the plot number to which this hashref applies (to support multi-range graphs), starting at zero.

x => the domain value for the plot element

y => the range value for the plot element

z => the Z axis value for 3-axis bar charts, if any

shape => the shape of the hotspot area of the plot element, same as for HTML: 'RECT', 'CIRCLE', 'POLY'

coordinates => an arrayref of the (x,y) pixel coordinates of the hotspot area to be mapped; for CIRCLE shape, its (x-center, y-center, radius), for RECT, its (upper-left corner x, upper-left corner y, lower-right corner x, lower-right corner y), and for POLY its the set of vertices (x,y)'s.

If the mapType is 'HTML', then either the mapURL or mapScript (or both) can be specified. mapURL specifies a legal URL string, e.g., 'http://www.mysite.com/cgi-bin/plotproc.pl?plotnum=:PLOTNUM&X=:X&Y=:Y', which will be added to the AREA tags generated for each mapped plot element. mapScript specifies any legal HTML scripting tag, e.g., 'ONCLICK="alert('Got X=:X, Y=:Y')"' to be added to each generated AREA tag.

For both mapURL and mapScript, special variables :PLOTNUM, :X, :Y, :Z can be specified which are replaced by the following values when the imagemap is generated.

Refer to the IMAGEMAP description at www.presicient.com/dbdchart#imagemap for details.

horizMargin, vertMargin

Sets the number of pixels around the actual plot area.

xAxisLabel, yAxisLabel, zAxisLabel

Sets the label strings for each axis.

xLog, yLog

When set to a non-zero value, causes the associated axis to be rendered in log10 format. Z axis plots are currently only symbolic, so no zLog is supported.

title

Sets a title string to be rendered at the bottom center of the image in bold text.

signature

Sets a string to be rendered in tiny font at the lower right corner of the image, e.g., 'Copyright(C) 2001, Presicient Corp.'.

legend

Set to an array ref of domain names to be displayed in a legend for the various plots. The legend is displayed below the chart, left justified and placed above the chart title string. The legend for each plot is printed in the same color as the plot. If a point shape or icon has been specified for a plot, then the point shape is printed with the label; otherwise, a small line segment is printed with the label. Due to space limitations, the number of datasets plotted should be limited to 8 or less.

showValues

When set to a non-zero value, causes the data points for each plotted element to be displayed next to hte plot point.

horizGrid, vertGrid

Causes grid lines to be drawn completely across the plot area.

xAxisVert

When set to a non-zero value, causes the X axis tick labels to be rendered vertically.

keepOrigin

When set to a non-zero value, forces the (0,0) data point into the graph. Normally, DBD::Chart::Plot will heuristically clip away from the origin is the plot never crosses the origin.

bgColor

Sets the background color of the image. Default is white.

threed

When set to a non-zero value for barcharts, causes the bars to be rendered in a 3-D effect.

icons

Set to an arrayref of image filenames. The images will be used to plot iconic barcharts or individual plot points, if the 'icon' shape is specified in the property string supplied to the setPoints() function (defined below). The array must match 1-to-1 with the number of plots in the image; icons and predefined point shapes can be mixed in the same image by setting the icon arrayref entry to undef for plots using predefined shapes in the properties string.

symDomain

When set to a non-zero value, causes the domain to be treated as discrete symbolic values which are evenly distributed over the X-axis. Numeric domains are plotted as scaled values in the image.

timeDomain

When set to a valid format string, the domain data points are treated as associated temporal values (e.g., date, time, timestamp, interval). The values supplied by setPoints will be strings of the specified format (e.g., 'YYYY-MM-DD'), but will be converted to numeric time values for purposes of plotting, so the domain is treated as continuous numeric data, rather than discrete symbolic. Note that for barcharts, histograms, candlesticks, or piecharts, temporal domains are treated as symbolic for plotting purposes, but are sorted as numeric values.

timeRange

When set to a valid format string, the range data points are treated as associated temporal values (e.g., date, time, timestamp, interval). The values supplied by setPoints will be strings of the specified format (e.g., 'YYYY-MM-DD'), but will be converted to numeric time values for purposes of plotting, so the range is treated as continuous numeric data.

gridColor

Sets the color of the axis lines and ticks. Default is black.

textColor

Sets the color used to render text in the image. Default is black.

font - NOT YET SUPPORTED

Sets the font used to render text in the image. Default is default GD fonts (gdMedium, gdSmall, etc.).

Specifies the name of an image file to be drawn into the background of the image. The logo image is centered in the plot image, and will be clipped if the logo size exceeds the defined width or height of the plot image.

By default, the graph will be centered within the image, with 50 pixel margin around the graph border. You can obtain more space for titles or labels by increasing the image size or increasing the margin values.

Establish data points: setPoints()

    $img->setPoints(\@xdata, \@ydata);
    $img->setPoints(\@xdata, \@ydata, 'blue line');
    $img->setPoints(\@xdata, \@ymindata, \@ymaxdata, 'blue points');
    $img->setPoints(\@xdata, \@ydata, \@zdata, 'blue bar zaxis');

Copies the input array values for later plotting. May be called repeatedly to establish multiple plots in a single graph. Returns a positive integer on success and undef on failure. The global graph properties should be set (via setOptions()) prior to setting the data points. The error() method can be used to retrieve an error message. X-axis values may be non-numeric, in which case the set of domain values is uniformly distributed along the X-axis. Numeric X-axis data will be properly scaled, including logarithmic scaling is requested.

If two sets of range data (ymindata and ymaxdata in the example above) are supplied, and the properties string does not specify a 3-axis barchart, a candlestick graph is rendered, in which case the domain data is assumed non-numeric and is uniformly distributed, the first range data array is used as the bottom value, and the second range data array is used as the top value of each candlestick. Pointshapes may be specified, in which case the top and bottom of each stick will be capped with the specified pointshape. The range and/or domain axis may be logarithmically scaled. If value display is requested, the range value of both the top and bottom of each stick will be printed above and below the stick, respectively.

Plot properties: Properties of each dataset plot can be set with an optional string as the third argument. Properties are separated by spaces. The following properties may be set on a per-plot basis (defaults in capitals):

    COLOR     CHARTSTYLE  USE POINTS?   POINTSHAPE
    -----     ---------  -----------   ----------
	BLACK       LINE        POINTS     FILLCIRCLE
	white      noline      nopoints    opencircle
	lgray       fill                   fillsquare
	gray        bar                    opensquare
	dgray       pie                    filldiamond
	lblue       box                    opendiamond
	blue       zaxis                   horizcross
	dblue      histo                   diagcross
	gold                               icon
	lyellow	                           dot
	yellow
	dyellow
	lgreen
	green
	dgreen
	lred
	red
	dred
	lpurple
	purple
	dpurple
	lorange
	orange
	pink
	dpink
	marine
	cyan
	lbrown
	dbrown

E.g., if you want a red scatter plot (red dots but no lines) with filled diamonds, you could specify

    $p->setPoints (\@xdata, \@ydata, 'Points Noline Red filldiamond');

Specifying icon for the pointshape requires setting the icon object attribute to a list of compatible image filenames (as an arrayref, see below). In that case, the icon images are displayed centered on the associated plotpoints. For 2-D barcharts, a stack of the icon is used to display the bars, including a proportionally clipped icon image to cap the bar if needed.

Draw the image: plot()

     $img->plot();

Draws the image and returns it as a string. To save the image to a file:

    open (WR,'>plot.png') or die ("Failed to write file: $!");
    binmode WR;            # for DOSish platforms
    print WR $img->plot();
    close WR;

To return the graph to a browser via HTTP:

    print "Content-type: image/png\n\n";
    print  $img->plot();

The range of values on each axis is automatically computed to optimize the data placement in the largest possible area of the image. As a result, the origin (0, 0) axes may be omitted if none of the datasets cross them at any point. Instead, the axes will be drawn on the left and bottom borders using the value ranges that appropriately fit the dataset(s).

Fetch the imagemap: getMap()

     $img->getMap();

Returns the imagemap for the chart. If no mapType was set, or if mapType was set to HTML. the returned value is a valid <MAP...><AREA...></MAP> HTML string. If mapType was set to 'Perl', a Perl-compatible arrayref declaration string is returned.

The resulting imagemap will be applied as follows:

2 axis 2-D Barcharts and Histograms

Each bar is mapped individually.

Piecharts

Each wedge is mapped. The CGI parameter values are used slightly differently than described above:

X=<wedge-label>&Y=<wedge-value>&Z=<wedge-percent>

3-D Barcharts (either 2 or 3 axis)

The top face of each bar is mapped. The Z CGI parameter will be empty for 2 axis barcharts.

3-D Histograms (either 2 or 3 axis)

The right face of each bar is mapped. The Z CGI parameter will be empty for 2 axis barcharts.

Line, point, area graphs

A 4 pixel diameter circle around each datapoint is mapped.

Candlestick graphs

A 4 pixel diameter circle around both the top and bottom datapoints of each stick are mapped.

Boxcharts

The area of the box is mapped, and 4-pixel diameter circles are mapped at the end of each extreme whisker.

Gantt Charts

The area of each bar in the chart is mapped.

TO DO

Top

programmable fonts
symbolic ranges for scatter graphs
axis labels for 3-D charts
surfacemaps
SVG support

AUTHOR

Top

Copyright (c) 2001 by Presicient Corporation. (darnold@presicient.com)

You may distribute this module under the terms of the Artistic License, as specified in the Perl README file.

SEE ALSO

Top

GD, DBD::Chart. (All available on CPAN).


DBD-Chart documentation Contained in the DBD-Chart distribution.

#23456789012345678901234567890123456789012345678901234567890123456789012345
#
# DBD::Chart::Plot -- Plotting engine for DBD::Chart
#
#	Copyright (C) 2001,2002 by Dean Arnold <darnold@presicient.com>
#
#   You may distribute under the terms of the Artistic License, 
#	as specified in the Perl README file.
#
#	Change History:
#
#	0.81	2005-Jan-26		D. Arnold
#		use TITLE instead of ALT in AREAMAPs
#		exit w/ error if compute scales/ranges would fail
#
#	0.80	2002-Sep-13		D. Arnold
#		programmable fonts
#		fix origin alignment for areagraphs w/ linegraphs
#		make image border programmable
#		fix 3-D piecharts where width >> height
#		improved 3-D piechart label positioning
#
#	0.73	2002-Sep-11		D. Arnold
#		fix scaling for 3-D bars w/ < 4 bars
#		fix all plots with single data point
#		add axis labels for 3-D bars
#		improve error reporting for iconic too wide,
#			icon format unsupported
#		fix for odd segmented quadtrees
#		fix scaling for 3-D bars when KEEPORIGIN=1
#		fix SHOWVALUES for 3-D bars when clipped from origin
#
#	0.72	2002-Aug-17		D. Arnold
#		fix showvalues for nonstacked bars/histos/candles
#		fix legend placement
#
#	0.71	2002-Aug-12		D. Arnold
#		add float property for bars/histos/areas
#		fix linewidth to be local property
#		fix bug in stacked areagraphs
#
#	0.70	2002-Jun-10		D. Arnold
#		add stacked bar, histo, area, and candlestick graphs
#		add quadtree graph
#		support new property keywords stack, showvalues
#		consolidated candlestick w/ 2D bars functions
#		add programmable linewidth to linegraph, candlesticks
#		add mapModifier callback
#		support NULL shapes entries
#
#	0.63	2002-May-16		D. Arnold
#		fix for Gantt chart date axis alignment
#
#	0.61	2002-Feb-07		D. Arnold
#		fix for :PLOTNUM imagemap variable in Gantt chart
#		fix for undef range values
#		added 'dot' point shape (contributed by Andrea Spinelli)
#		fix for temporal alignment
#		fix for tick labels overwriting axis labels
#
#	0.60	2002-Jan-12		D. Arnold
#		support temporal datatypes
#		support histograms
#		support composite images
#		support user defined colors
#		scale boxchart vertical offsets
#		support Gantt charts
#
#	0.52	2001-Dec-14		D. Arnold
#		fix for ymax in 2d bars
#
#	0.51	2001-Dec-01		D. Arnold
#		Support multicolor barcharts
#		Support 3D piecharts
#
#	0.50	2001-Oct-14		 D. Arnold
#		Add barchart, piechart engine
#		Add iconic barcharts, pointshapes
#		Add 3D, 3 axis barcharts
#		Add HTML imagemap generation
#		Increase axis label text length
#
#	0.43	2001-Oct-11		 P. Scott
#		Allow a 'gif' (or any future format supported by
#		GD::Image) format to be called in plot().
#
#	0.42	2001-Sep-29		Dean Arnold
#		- fixed xVertAxis handling for candlestick and symbolic domains
#
#	0.30	Jun 1, 2001		Dean Arnold
#		- fixed Y-axis tick problem when no grid used
#
#	0.20	Mar 10, 2001	Dean Arnold
#		- added logrithmic graphs
#		- added area graphs
#		- added image overlays
#
#	0.10	Feb 20, 2001	Dean Arnold
#		- Coded.
#
require 5.6.0;
use strict 'vars';

{
package DBD::Chart::Plot;

use GD;
use GD::Text;
use GD::Text::Align;
use Time::Local;
use GD qw(gdBrushed gdSmallFont gdTinyFont gdMediumBoldFont);

$DBD::Chart::Plot::VERSION = '0.81';

#
#	list of valid colors
#
our @clrlist = qw(
	white lgray	gray dgray black lblue blue dblue gold lyellow	
	yellow	dyellow	lgreen	green dgreen lred red dred lpurple	
	purple dpurple lorange orange pink dpink marine	cyan	
	lbrown dbrown );
#
#	RGB of valid colors
#
our %colors = (
	white	=> [255,255,255], 
	lgray	=> [191,191,191], 
	gray	=> [127,127,127],
	dgray	=> [63,63,63],
	black	=> [0,0,0],
	lblue	=> [0,0,255], 
	blue	=> [0,0,191],
	dblue	=> [0,0,127], 
	gold	=> [255,215,0],
	lyellow	=> [255,255,0], 
	yellow	=> [191,191,0], 
	dyellow	=> [127,127,0],
	lgreen	=> [0,255,0], 
	green	=> [0,191,0], 
	dgreen	=> [0,127,0],
	lred	=> [255,0,0], 
	red		=> [191,0,0],
	dred	=> [127,0,0],
	lpurple	=> [255,0,255], 
	purple	=> [191,0,191],
	dpurple	=> [127,0,127],
	lorange	=> [255,183,0], 
	orange	=> [255,127,0],
	pink	=> [255,183,193], 
	dpink	=> [255,105,180],
	marine	=> [127,127,255], 
	cyan	=> [0,255,255],
	lbrown	=> [210,180,140], 
	dbrown	=> [165,42,42],
	transparent => [1,1,1]
);
#
#	pointshapes
#
our %valid_shapes = (
'fillsquare', 1,
'opensquare', 2,
'horizcross', 3,
'diagcross', 4,
'filldiamond', 5,
'opendiamond', 6,
'fillcircle', 7,
'opencircle', 8,
'icon', 9,
'dot', 10,
'null', 11);

#
#	logarithmic steps for axis scaling
#
our @logsteps = (0, log(2)/log(10), log(3)/log(10), log(4)/log(10), 
	log(5)/log(10), 1.0);
#
#	index of vertex pts for 3-D barchart
#	polygonal visible faces
#
our @polyverts = ( 
[ 1*2, 2*2,	3*2, 4*2 ],	# top face
[ 0*2, 1*2, 4*2, 5*2 ],	# front face
[ 4*2, 3*2, 6*2, 5*2 ]	# side face
);
#
# indices of 3-D projection vertices
#	mapped to line segments
#
our @vert2lines = (
1*2, 4*2, 	# top front l-r
0*2, 1*2,	# left front b-t
0*2, 5*2,	# bottom front l-r
4*2, 5*2,	# right front b-t
1*2, 2*2,	# top left f-r
2*2, 3*2,	# top rear l-r
3*2, 4*2,   # right top r-f
3*2, 6*2,   # right rear t-b
5*2, 6*2,   # right bottom r-f
);

#
#	indices of 3-D projection of axes planes
#
our @axesverts = (
	0*2, 1*2,	# left wall
	1*2, 3*2,
	3*2, 2*2,
	2*2, 0*2,
		
# rear wall
	3*2, 6*2,	# trl to trr
	6*2, 5*2,	# trr to brr
	5*2, 1*2,	# brr to brl

# floor		
	9*2, 10*2,	# brr to brl
	10*2, 7*2,	# brl to brf
	
	9*2, 8*2,	# brr to brf
	7*2, 8*2,	# blf to brf
);
#
#	font sizes
#
our ($sfw,$sfh) = (gdSmallFont->width, gdSmallFont->height);
our ($tfw,$tfh) = (gdTinyFont->width, gdTinyFont->height);

our %valid_attr = qw(
	width 1
	height 1
	genMap 1
	mapType 1
	mapURL 1
	mapScript 1
	horizMargin 1
	vertMargin 1
	xAxisLabel 1
	yAxisLabel 1
	zAxisLabel 1
	xLog 1
	yLog 1
	zLog 1
	title 1
	signature 1
	legend 1
	horizGrid 1
	vertGrid 1
	xAxisVert 1
	keepOrigin 1
	bgColor 1
	threed 1
	icons 1
	symDomain 1
	timeDomain 1
	gridColor 1
	textColor 1
	font 1
	logo 1
	timeRange 1
	mapModifier 1
	border 1
);
our @lines = ( 
[ 0*2, 4*2,	5*2, 1*2 ],	# top face
[ 0*2, 1*2, 3*2, 2*2 ],	# front face
[ 1*2, 5*2, 7*2, 3*2 ]	# side face
);

our %gdfontmap = (
5, gdTinyFont,
6, gdSmallFont,
7, gdMediumBoldFont,
8, gdLargeFont,
9, gdGiantFont
);

our %fontMap = ();

our %month = ( 'JAN', 0, 'FEB', 1, 'MAR', 2, 'APR', 3, 'MAY', 4, 'JUN', 5, 
'JUL', 6, 'AUG', 7, 'SEP', 8, 'OCT', 9, 'NOV', 10, 'DEC', 11);
our @monthmap = qw( JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC );
#
#	URI escape map
#
our %escapes = ();
for (0..255) {
    $escapes{chr($_)} = sprintf("%%%02X", $_);
}

use constant LINE => 1;
use constant POINT => 2;
use constant AREA => 4;
use constant BOX => 8;
use constant PIE => 16;
use constant HISTO => 32;
use constant BAR => 64;
use constant CANDLE => 128;
use constant GANTT => 256;
use constant QUADTREE => 512;

our %typemap = ( 'BAR', BAR, 'HISTO', HISTO, 'FILL', AREA, 
	'CANDLE', CANDLE, 'BOX', BOX, 'GANTT', GANTT, 'QUADTREE', QUADTREE );

sub new {
    my $class = shift;
    my $obj = {};
    bless $obj, $class;
    $obj->init (@_);

    return $obj;
}
#
#	Plot object members:
#
#	img - GD::Image object
#	width - image width in pixels
#	height - image height in pixels
#	signature - signature string
#	genMap - 1 => generate HTML imagemap
#	horizMargin - image horizontal margins in pixels
#	vertMargin - image vertical margins in pixels
#	data - data points to be plotted
#	props - graph properties
#	plotCnt - number of plots in graph
#	xl, xh, yl, yh, zl, zh - min/max of each axis
#	xscale, yscale, zscale - scaling factors for assoc. axis
#	horizEdge, vertEdge - horizontal/vertical edge location
#	horizStep, vertStep - horizontal/vertical pixel increment
#	haveScale - calculated min/max is valid
#	xAxisLabel, yAxisLabel, zAxisLabel - label for assoc. axis
#	title - title string
#	xLog, yLog, zLog - 1 => assoc axis is logarithmic
#	errmsg - last error msg
#	keepOrigin - force (0,0[,0]) into graph
#	imgMap - HTML imagemap text
#	symDomain - 1 => domain is symbolic
#	timeDomain - 1 => domain is temporal
#	icon - name of icon image file for iconic barcharts/points
#	logo - name of background logo image file
#	mapModifier - ref to callback to modify imagemap entries
#	_legends - arrayref to hold legend info til we render it
#
sub init {
	my ($obj, $w, $h, $colormap) = @_;

	$w = 400 unless $w;
	$h = 300 unless $h;
	my $img = new GD::Image($w, $h);
#
#	if a colormap supplied, copy it into our color list
#
	if ($colormap) {
		foreach my $color (keys(%$colormap)) {
			$colors{lc $color} = $$colormap{$color};
		}
	}
	
	my $white = $img->colorAllocate(@{$colors{white}});
	my $black = $img->colorAllocate(@{$colors{black}}); 

	$obj->{width} = $w;
	$obj->{height} = $h;
	$obj->{img} = $img;

# imagemap attributes
  	$obj->{genMap} = undef;	# name of map
  	$obj->{imgMap} = '';		# contains resulting map text
  	$obj->{mapType} = 'HTML';	# default HTML
  	$obj->{mapURL} = '';		# base URL for hotspots
  	$obj->{mapScript} = '';	# base script call for hotspots
  		
# image margins
	$obj->{horizMargin} = 50;
	$obj->{vertMargin} = 70;

# create an empty array for point arrays and properties
	$obj->{data} = [ ];
	$obj->{props} = [ ];
	$obj->{plotCnt} = 0;
	$obj->{plotTypes} = 0;

# used for pt2pxl()
	$obj->{xl} = undef;
	$obj->{xh} = undef;
	$obj->{yl} = undef;
	$obj->{yh} = undef;
	$obj->{zl} = undef;
	$obj->{zh} = undef;
	$obj->{xscale} = 0;
	$obj->{yscale} = 0;
	$obj->{zscale} = 0;
	$obj->{horizEdge} = 0;
	$obj->{vertEdge} = 0;
	$obj->{horizStep} = 0;
	$obj->{vertStep} = 0;
	$obj->{Xcard} = 0;		# cardinality of 3-axis barcharts
	$obj->{Zcard} = 0;
	$obj->{plotWidth} = 0;	# true plot width; height; depth
	$obj->{plotHeight} = 0;
	$obj->{plotDepth} = 0;
	$obj->{brushWidth} = 0; # width of bars or candlesticks
	$obj->{brushDepth} = 0;
	$obj->{rangeSum} = 0;	# running total for piecharts
	$obj->{haveScale} = 0;	# 1 = last calculated min & max still valid
	$obj->{domainValues} = { };	# map of domain values for bar/histo/candle/sym domains
	$obj->{boxCount} = 0;	# num of boxcharts in plot
	$obj->{barCount} = 0;	# num of barchart/histos in plot
	$obj->{xMaxLen} = 0;	# max length of symbolic X value
	$obj->{yMaxLen} = 0;	# max length of temporal Y value
	$obj->{zMaxLen} = 0;	# max length of symbolic Z value

# axis label strings
	$obj->{xAxisLabel} = '';
	$obj->{yAxisLabel} = '';
	$obj->{zAxisLabel} = '';

	$obj->{xLog} = 0;		# 1 => log10 scaling
	$obj->{yLog} = 0;
	$obj->{zLog} = 0;

	$obj->{title} = '';
	$obj->{signature} = '';
	$obj->{legend} = 0; 	# 1 => render legend
	$obj->{horizGrid} = 0;	# 1 => print y-axis gridlines
	$obj->{vertGrid} = 0;	# 1 => print x-axis gridlines
	$obj->{xAxisVert} = 0;	# 1 => print x-axis label vertically
	$obj->{errmsg} = '';	# error result of last operation
	$obj->{keepOrigin} = 0; # 1 => force origin into graph
	$obj->{threed} = 0;		# 1 => use 3-D effect
	$obj->{logo} = undef;
		
	$obj->{icons} = [ ];	# array of icon filenames
		
	$obj->{symDomain} = 0;	# 1 => use symbolic domain
	$obj->{timeDomain} = undef; # defines format of temporal domain labels
	$obj->{timeRange} = undef; # defines format of temporal range labels

#  allocate some oft used colors
	$obj->{white} = $white;
	$obj->{black} = $black; 
	$obj->{transparent} = $img->colorAllocate(@{$colors{'transparent'}});

	$obj->{bgColor} = $white; # background color
	$obj->{gridColor} = $black;
	$obj->{textColor} = $black;
	$obj->{border} = 1;

	$obj->{mapModifier} = undef;
	
#	for now these aren't used, but someday we'll let them be configured
	$obj->{font} = 'gd';

	$obj->{_legends} = [ ];

# set image basic properties
	$img->transparent($obj->{transparent});
	$img->interlaced('true');
#	$img->rectangle( 0, 0, $w-1, $h-1, $obj->{black});
}

#
#	compare function for numeric sort
#
sub numerically { $a <=> $b }

sub convert_temporal {
	my ($value, $format) = @_;
#
#	use Perl funcs to compute seconds from date
	my $t;
	$t = timegm(0, 0, 0, $3, $2 - 1, $1),
	$t -= ($t%86400), #	timelocal isn't behaving quite right
	return $t
		if (($format eq 'YYYY-MM-DD') &&
			($value=~/^(\d+)[\-\.\/](\d+)[\-\.\/](\d+)$/));

	$t = timegm(0, 0, 0, $3, $month{uc $2}, $1),
	$t -= ($t%86400), #	timelocal isn't behaving quite right
	return $t
		if (($format eq 'YYYY-MM-DD') &&
			($value=~/^(\d+)[\-\.\/](\w+)[\-\.\/](\d+)$/) &&
			defined($month{uc $2}));

	return timegm($6, $5, $4, $3, $2 - 1, $1) + ($7 ? $7 : 0)
		if (($format eq 'YYYY-MM-DD HH:MM:SS') &&
			($value=~/^(\d+)[\-\.\/](\d+)[\-\.\/](\d+)\s+(\d+):(\d+):(\d+)(\.\d+)?$/));

	return timegm($6, $5, $4, $3, $month{uc $2}, $1) + ($7 ? $7 : 0)
		if (($format eq 'YYYY-MM-DD HH:MM:SS') &&
			($value=~/^(\d+)[\-\.\/](\w+)[\-\.\/](\d+)\s+(\d+):(\d+):(\d+)(\.\d+)?$/) &&
			(defined($month{uc $2})));

	return (($1 ? (($1 eq '-') ? -1 : 1) : 1) * (($3 ? ($3 * 3600) : 0) + ($5 ? ($5 * 60) : 0) + 
		$6 + ($7 ? $7 : 0)))
		if ((($format eq '+HH:MM:SS') || ($format eq 'HH:MM:SS')) && 
			($value=~/^([\-\+])?((\d+):)?((\d+):)?(\d+)(\.\d+)?$/));

	return undef; # for completeness, shouldn't get here
}
#
#	restore the readable datetime form from 
#	the input numeric value
sub restore_temporal {
	my ($value, $format) = @_;

	my ($sign, $subsec, $sec, $min, $hour, $mday, $mon, $yr, $wday, $yday, $isdst);
	$sign = ($value < 0);
	$value = abs($value);
	if (($format eq '+HH:MM:SS') || ($format eq 'HH:MM:SS')) {
		$hour = int($value/3600);
		$min = int(($value%3600)/60);
		$sec = int($value%60);
		$hour = "0$hour" if ($hour < 10);
		$min = "0$min" if ($min < 10);
		$sec = "0$sec" if ($sec < 10);
		$subsec = int(($value - int($value)) * 100);
		return ($sign ? '-' : '') . "$hour:$min:$sec" . 
			($subsec ? ".$subsec" : '');
	}

	($sec, $min, $hour, $mday, $mon, $yr, $wday, $yday, $isdst) = gmtime($value);
	$yr += 1900;
	$mon++;
	$mon = "0$mon" if ($mon < 10);
	$min = "0$min" if ($min < 10);
	$sec = "0$sec" if ($sec < 10);
	$mday = "0$mday" if ($mday < 10);
	
	return "$yr\-$mon\-$mday"
		if ($format eq 'YYYY-MM-DD');

	$mon = $monthmap[$mon-1],
	return "$yr\-$mon\-$mday"
		if ($format eq 'YYYY-MMM-DD');

	return "$yr\-$mon\-$mday $hour:$min:$sec"
		if ($format eq 'YYYY-MM-DD HH:MM:SS');

	$mon = $monthmap[$mon-1],
	return "$yr\-$mon\-$mday $hour:$min:$sec"
		if ($format eq 'YYYY-MMM-DD HH:MM:SS');

	return undef; # for completeness, shouldn't get here
}

sub set3DBarPoints {
	my ($obj, $xary, @ranges) = @_;
	my $type = pop @ranges;
	my $props = pop @ranges;
	my ($yary, $zary);
	my ($ymin, $ymax) = ($obj->{yl}, $obj->{yh});
	my $ymaxlen = 0;
#
#	verify:
#		2 range sets if 3-axis
#		each rangeset has same number of elements as domain
#
	my $hasZaxis = ($obj->{zAxisLabel});
	$zary = pop @ranges
		if $hasZaxis;
	
	my @zs = ();
	my %zhash = ();
	my %xhash = ();
	my @xs = ();
	my @xvals = @$xary;
	my $ys;
	my ($xval, $zval) = (0,1);
	my $i = 0;
	my ($x, $y, $z);
	my $maxlen = 0;
#
#	collect all X's and convert if needed
	foreach (0..$#xvals) {
		$xvals[$_] = convert_temporal($xvals[$_], $obj->{timeDomain})
			if $obj->{timeDomain};
		next if $xhash{$xvals[$_]};
		push(@xs, $xvals[$_]);
		$xhash{$xvals[$_]} = 1;
		$maxlen = length($xvals[$_]) if (length($xvals[$_]) > $maxlen);
	}
	$obj->{xMaxLen} = $maxlen;

	foreach (@ranges) {
		$obj->{errmsg} = 'Unbalanced dataset.',
		return undef
			if ($#$xary != $#$_);
	}

	if ($hasZaxis) {
#
#	only 1 3-axis dataset permitted
		$obj->{errmsg} = 'Incompatible plot types.', return undef
			if $obj->{plotTypes};

		$obj->{errmsg} = 'Unbalanced dataset.',
		return undef
			if ($#$xary != $#$zary);
#
#	collect distinct Z and X values, and correlate them
#	with the assoc. Y value via hashes
#
		$maxlen = 0;
		foreach $z (0..$#$zary) {
			$zval = $$zary[$z];
			push(@zs, $zval),
			$zhash{$zval} = { }
				unless $zhash{$zval};
			$zhash{$zval}->{$xvals[$z]} = [ 0 ] unless $zhash{$zval}->{$xvals[$z]};
			$ys = $zhash{$zval}->{$xvals[$z]};
			foreach (@ranges) {
				$ymaxlen = length($$_[$z]) if (length($$_[$z]) > $ymaxlen);
				push(@$ys, $$_[$z]);
			}
			$maxlen = length($zval) if (length($zval) > $maxlen);
		}
		$obj->{zMaxLen} = $maxlen;
	}
	else {
		$obj->{errmsg} = 'Incompatible plot types.', return undef
			unless (($obj->{plotTypes} == 0) || ($obj->{plotTypes} & $type));
#
#	synthesize Z axis values so we can process same as true 3 axis
#
		push(@zs, 1);
		$zhash{1} = { };
		foreach $x (0..$#$xary) {
			$zhash{1}->{$xvals[$x]} = [ 0 ] unless $zhash{1}->{$xvals[$x]};
			$ys = $zhash{1}->{$xvals[$x]};
			foreach (@ranges) {
				$ymaxlen = length($$_[$x]) if (length($$_[$x]) > $ymaxlen);
				push(@$ys, $$_[$x]);
			}
		}
		$obj->{zMaxLen} = 0;
	}
	
	@xs = sort numerically @xs
		if $obj->{timeDomain};

	$obj->{plotTypes} |= $type;
	$obj->{zValues} = \@zs;
	$obj->{xValues} = \@xs;
#
#	sort datapoints in order Z from back to front,
#		X from left to right
#	(i.e., GROUP BY Z, X ORDER BY Z DESCENDING, X ASCENDING)
#	the order of appearance in the input arrays determines
#	what "front, back, left, and right" mean
#
#	Since X and Z are always symbolic, we generate numeric pseudo values
#	for them based on order of appearance in the input arrays
#
	my $zCard = scalar @zs;	# go from last Z value forward
	my $xCard = scalar @xs;
	my ($znum, $xnum) = (0,0,0);
	my @ary = ();
	my $lasty;
	my $j;
	for (my $z = $zCard; $z > 0; $z--) {
		foreach $x (1..$xCard) {
			$ys = $zhash{$zs[$z-1]}->{$xs[$x-1]};
#
#	adapt for neg to pos swings
#
			if (($$ys[1] < 0) && (($#$ys == 1) || ($$ys[$#$ys] >= 0))) {
				$i = 1;
				$$ys[$i-1] = $$ys[$i], 
				$i++ 
					while (($i <= $#$ys) && ($$ys[$i] < 0));
				$$ys[$i-1] = 0;
			}
#
#	data is stored in output array as (X, [ Ymin..Ymax ], Z, ...)
#
			$lasty = 0;
			my $starts = ($$ys[0] == 0) ? 1 : 0;
			foreach $i ($starts..$#$ys) {
				$y = $$ys[$i];
				$y = convert_temporal($y, $obj->{timeRange}) 
					if $obj->{timeRange};

				$obj->{errmsg} = "Non-numeric range value $y.",
				return undef
					unless ($obj->{timeRange} || 
						($y=~/^[+-]?\.?\d\d*(\.\d*)?([Ee][+-]?\d+)?$/));
		
				$obj->{errmsg} = 
					'Negative value supplied for logarithmic axis.',
				return undef
					if (($obj->{yLog}) && ($y <= 0));
#
#	on - to + transition, reset increment
				$lasty = 0 if (($lasty < 0) && ($y >= 0));

				$y += $lasty;
				$y = log($y)/log(10) if ($obj->{yLog});
				$$ys[$i] = $y;
				$lasty = $y;

				$ymin = $y unless (defined($ymin) && ($ymin <= $y));
				$ymax = $y unless (defined($ymax) && ($ymax >= $y));
			}
			push(@ary, $x, $ys, $z);
		}
	}
# record the dataset; use stack to support multi-graph images
	push(@{$obj->{data}}, \@ary);
	push(@{$obj->{props}}, $props);
	$obj->{xl} = 1;
	$obj->{xh} = $xCard;
	$obj->{yl} = $ymin;
	$obj->{yh} = $ymax;
	$obj->{zl} = 1;
	$obj->{zh} = $zCard;
	$obj->{Xcard} = $xCard;
	$obj->{Zcard} = $zCard;
	$obj->{haveScale} = 0;	# invalidate prior min-max calculations
	$obj->{barCount}++;
	$obj->{symDomain} = 0;	# to avoid a later sort
	$obj->{yMaxlen} = $ymaxlen;
	return 1;
}

sub set2DBarPoints {
	my ($obj, $xary, @ranges) = @_;

	my $type = pop @ranges;
	my $props = pop @ranges;
	my $lwidth = 2;
	$lwidth = $1 if ($props=~/\bwidth:(\d+)/i);
	my $unanchored = ($props=~/\bfloat\b/i);
	
	foreach (@ranges) {
		$obj->{errmsg} = 'Unbalanced dataset.',
		return undef
			if ($#$xary != $#$_);
	}
#
#	validate environment
#
	$obj->{errmsg} = 'Candlesticks require a minimum and maximum range value.', 
	return undef
		unless (($type != CANDLE) || ($#ranges > 0));

	$obj->{errmsg} = 'Unanchored charts require a minimum and maximum range value.', 
	return undef
		if ($unanchored && ($#ranges <= 0));

	$obj->{errmsg} = 'Incompatible plot types.', return undef
		if ((($type == HISTO) && $obj->{plotTypes} && ($obj->{plotTypes}^HISTO)) ||
			(($type != HISTO) && ($obj->{plotTypes} & HISTO)));

	$obj->{errmsg} = 'Incompatible plot domain types.', return undef
		if (($obj->{plotTypes} & (BOX|PIE|GANTT|QUADTREE)) ||
			(($obj->{plotTypes} & (LINE|POINT|AREA)) && (! $obj->{symDomain})));

	$obj->{symDomain} = 1;
	$obj->{plotTypes} |= $type;
	
	my ($x, $y) = (0,0,0,0);
	my $ty = 0;
	my ($ymin, $ymax) = ($obj->{yl}, $obj->{yh});
	$ymin = 1E38 unless $ymin;
	$ymax = -1E38 unless $ymax;
#
# record/merge the dataset
	my $domVals = $obj->{domainValues};
	my @data = ();
	my $idx = 0;
	my $i;
	for ($i = 0; $i <= $#$xary; $i++) {
#
#	eliminate undefined data points
#
		next unless defined($$xary[$i]);
		next if (($type == CANDLE) && (! defined($ranges[0]->[$i])));

		$x = $$xary[$i];
		$x = convert_temporal($x, $obj->{timeDomain}) if $obj->{timeDomain};

		$domVals->{$x} = defined($domVals) ? scalar(keys(%$domVals)) : 0
			unless defined($domVals->{$x});
#
#	force data into array in same order as any prior definition
		$idx = $domVals->{$x} * 3;
		$data[$idx++] = $x;
		$data[$idx] = [ ];
		my $yary = $data[$idx];

		$obj->{xMaxLen} = length($x) 
			unless ($obj->{xMaxLen} && ($obj->{xMaxLen} >= length($x)));
#
#	validate the range values
		my $lasty = 0;
#
#	reserve the first element for our 'base' value
#	to be determined after we compute the first 2 range elements
		push(@$yary, undef) unless (($type == CANDLE) || $unanchored);
		my $first_valid = 0;
		foreach (@ranges) {
			$y = $_->[$i];
#
#	to support skipping some ranges
			push(@$yary, undef), next unless defined($y);
			
			$y = convert_temporal($y, $obj->{timeRange})
				if $obj->{timeRange};
			$first_valid++;

			$obj->{errmsg} = 'Non-numeric range value ' . $y . '.',
			return undef
				unless ($obj->{timeRange} || 
					($y=~/^[+-]?\d+\.?\d*([Ee][+-]?\d+)?$/));

			$obj->{errmsg} = 'Invalid value supplied for logarithmic axis.',
			return undef
				if ($obj->{yLog} && ($y <= 0));
#
#	compute cumulative range value
			push(@$yary, 0), $lasty = 0
				if (($lasty < 0) && ($y >= 0) && (! $unanchored));
			$y += $lasty;
			push(@$yary, ($obj->{yLog} ? log($y)/log(10) : $y));
			$lasty = $y unless (($unanchored || ($type == CANDLE)) && ($first_valid == 1));
			$ymin = $y unless (defined($ymin) && ($y >= $ymin));
			$ymax = $y unless (defined($ymax) && ($y <= $ymax));
		}
#
#	now determine the base value:
#	if we start negative, but go positive, then just use first
#	range value as base
#
		unless ($unanchored || ($type == CANDLE)) {
			if (($$yary[1] < 0) && (($#$yary == 1) || ($$yary[2] > $$yary[1]))) {
				push @$yary, 0
					if ($#$yary == 1);

				shift @$yary;	# no need for placeholder
				next;
			}
#
#	everything else starts from zero
#
			$$yary[0] = 0;
		}
	}
	push(@{$obj->{data}}, \@data);
	push(@{$obj->{props}}, $props);
	$obj->{yl} = $ymin;
	$obj->{yh} = $ymax;
	$obj->{xl} = 1;
	$obj->{xh} = scalar(keys(%$domVals));
	
	$obj->{haveScale} = 0;	# invalidate any prior min-max calculations
	$obj->{barCount}++;
	$obj->{brushWidth} = $lwidth
		if ($type == CANDLE);

	return 1;
}

sub setPiePoints {
	my ($obj, $xary, $yary, $props) = @_;
		
	my @ary = ();

	$obj->{errmsg} = 'Incompatible plot types.', return undef
		if $obj->{plotTypes};

	$obj->{errmsg} = 'Unbalanced dataset.',
	return undef
		if ($#$xary != $#$yary);
	
	my $xtotal = 0;
	my ($i, $y);
	foreach (0..$#$xary) {
		next unless (defined($$xary[$_]) && defined($$yary[$_]));
		$y = $$yary[$_];
		$y = convert_temporal($y, $obj->{timeRange}) if $obj->{timeRange};

		$obj->{errmsg} = 'Non-numeric range value ' . $y . '.',
		return undef
			unless ($obj->{timeRange} || 
				($y=~/^[+-]?\.?\d+\.?\d*([Ee][+-]?\d+)?$/));

		$obj->{errmsg} = 
			'Negative range values not permitted for piecharts.',
		return undef
			if ($y < 0);

		$xtotal += $y;
		push(@ary, $$xary[$_], $y);
	}
	$obj->{plotTypes} |= PIE;
	push(@{$obj->{data}}, \@ary);
	push(@{$obj->{props}}, $props);
	$obj->{rangeSum} = $xtotal;
	$obj->{haveScale} = 0; # invalidate any prior min-max calculations
	return 1;
}

sub setBoxPoints {
	my ($obj, $xary, $props) = @_;

	$obj->{errmsg} = 'Incompatible plot types.', return undef
		if ($obj->{plotTypes} & (PIE|HISTO|BAR|CANDLE|GANTT|QUADTREE));
		
	$obj->{errmsg} = 'Boxchart not compatible with 3-D plot types.', return undef
		if ($obj->{threed} || $obj->{zAxis});
		
	$obj->{errmsg} = 'Boxchart not compatible with symbolic domains.', return undef
		if $obj->{symDomain};
		
	my @data = ();
	foreach (@$xary) {
		next unless defined($_);
		$_ = convert_temporal($_, $obj->{timeDomain}) if $obj->{timeDomain};
		$obj->{errmsg} = 'Non-numeric value ' . $_ . '.',
		return undef
			unless ($_=~/^[+-]?\d+\.?\d*([Ee][+-]?\d+)?$/);
		push(@data, $_);
	}
	@data = sort numerically @data;
	$obj->{xl} = $data[0] 
		unless (defined($obj->{xl}) && ($data[0] >= $obj->{xl}));
	$obj->{xh} = $data[$#data] 
		unless (defined($obj->{xh}) && ($data[$#data] <= $obj->{xh}));
	push(@{$obj->{data}}, \@data);
	push(@{$obj->{props}}, $props);
	$obj->{boxCount}++;
	$obj->{plotTypes} |= BOX;
	$obj->{numRanges} = 0;
	$obj->{haveScale} = 0; # invalidate any prior min-max calculations
	return 1;
}
#
#	variable arglist:
#
#	for line/point/area graphs, 2-axis barcharts:
#		setPoints($plotobj, \@xarray, \@yarray1, $props)
#	for 3-axis barcharts, surfacemaps:
#		setPoints($plotobj, \@xarray, \@yarray, \@zarray, $props)
#	for candlesticks, barcharts:
#		setPoints($plotobj, \@xarray, \@ylow, \@yhigh, $props)
#	for piecharts:
#		setPoints($plotobj, \@xarray, \@yarray, $props)
#	for box&whisker:
#		setPoints($plotobj, \@xarray, $props)
#	for Gantt:
#		setPoints($plotobj, \@tasks, \@start,\@end, \@assigned, \@pctcomplete,
#		\@depend1, [\@dependent2...], $props)
#
#	NOTE: graph type properties must be set prior to setting graph points
#	Each domain/rangeset must be separately defined with its properties
#	(e.g., a barchart with N domains requires N setPoints calls)
#
sub setPoints {
	my ($obj, $xary, @ranges) = @_;
	my $props = pop @ranges;
#
#	new stacked bars/candlesticks/histos
#
	return $obj->set3DBarPoints($xary, @ranges, $props, $typemap{uc $1})
		if (($props=~/\b(bar|histo)\b/i) && 
			($obj->{zAxisLabel} || $obj->{threed}));

	return $obj->set2DBarPoints($xary, @ranges, $props, $typemap{uc $1})
		if ($props=~/\b(bar|histo|candle)\b/i);

	return $obj->setPiePoints($xary, @ranges, $props)
		if ($props=~/\bpie\b/i);

	return $obj->setBoxPoints($xary, @ranges, $props)
		if ($props=~/\bbox\b/i);

	return $obj->setGanttPoints($xary, @ranges, $props)
		if ($props=~/\bgantt\b/i);

	return $obj->setQuadPoints($xary, @ranges, $props)
		if ($props=~/\bquadtree\b/i);
#
#	must be line/point/area, verify ranges have same num of elements
#	as domain
#
	$obj->{errmsg} = 'Incompatible plot types.', return undef
		if ($obj->{plotTypes} & (PIE|HISTO|GANTT|QUADTREE));
		
	$obj->{errmsg} = 
		'Line/point/area graph not compatible with 3-D plot types.', return undef
		if ($obj->{threed} || $obj->{zAxis});

	my ($x, $y, $yary) = (0,0, [ ]);
	my ($xmin, $xmax, $ymin, $ymax) = 
		($obj->{xl}, $obj->{xh}, $obj->{yl}, $obj->{yh});
	my $is_symbolic = $obj->{symDomain};
	$xmin = $is_symbolic ? 1 : 1E38 unless defined($xmin);
	$xmax = $is_symbolic ? $#$xary + 1 : -1E38 unless defined($xmax);
	$ymin = 1E38 unless defined($ymin);
	$ymax = -1E38 unless defined($ymax);
	
	if (($props=~/\bstack\b/i) && ($props=~/\bfill\b/i)) {

		foreach (@ranges) {
			$obj->{errmsg} = 'Unbalanced dataset.',
			return undef
				if ($#$xary != $#$_);
		}
#
#	condense the stacked values
#
		foreach my $i (0..$#$xary) {
			$$yary[$i] = [ ];
			my $ys = $$yary[$i];
			my $lasty = 0;
			my $first_valid = 0;
			foreach (@ranges) {

				$y = $$_[$i];
				next unless defined($y);
				$y = convert_temporal($y, $obj->{timeRange}) 
					if $obj->{timeRange};
#
#	validate the range values
				$obj->{errmsg} = 'Non-numeric range value ' . $y . '.',
				return undef
					unless ($y=~/^[+-]?\.?\d+\.?\d*([Ee][+-]?\d+)?$/);
		
				$obj->{errmsg} = 
					'Invalid value supplied for logarithmic axis.',
				return undef
					if ($obj->{yLog} && ($y <= 0));

				$first_valid++;
				
				$y += $lasty;
				$y = log($y)/log(10) if $obj->{yLog};
				$ymin = $y if ($y < $ymin);
				$ymax = $y if ($y > $ymax);
				push @$ys, $y;
				$lasty = $y unless (($props=~/\bfloat\b/i) && ($first_valid == 1));
			}
		}
	}
	else {
		$yary = $ranges[0];

		$obj->{errmsg} = 'Unbalanced dataset.',
		return undef
			if ($#$xary != $#$yary);
	}
#
# record/merge the dataset
	my $domVals = $obj->{domainValues};
	my @data = ();
	my $idx = 0;
	my @xs = ();
	my @ys = ();
#
#	sort numeric/temporal domain into asc. order
#
	my %xhash = ();
	my $needsort = 0;
	foreach (0..$#$xary) {
#
#	eliminate undefined data points
#
		$x = $$xary[$_];
		next unless defined($x);
		
		$x = convert_temporal($x, $obj->{timeDomain}) if $obj->{timeDomain};

		$obj->{errmsg} = "Non-numeric domain value $x.",
		return undef
			unless ($is_symbolic ||
				($x=~/^[+-]?\.?\d+\.?\d*([Ee][+-]?\d+)?$/));

		$obj->{errmsg} = "Invalid value for logarithmic axis.",
		return undef
			unless ($is_symbolic || (! $obj->{xLog}) ||	($x > 0));

		$obj->{xMaxLen} = length($x) 
			if ($is_symbolic && ((! $obj->{xMaxLen}) || (length($x) > $obj->{xMaxLen})));
		push(@xs, $x),
		$xhash{$x} = $#xs,
		next
			if $is_symbolic;

		$x = log($x)/log(10) if $obj->{xLog};
		$needsort = 1 if (($#xs >= 0) && ($xs[$#xs] > $x));
		push @xs, $x;
		$xhash{$x} = $#xs;
	}
#
#	optimize for presorted domains
	@ys = @$yary 
		unless $needsort;

	if ($needsort) {
		@xs = sort numerically @xs ;
		push @ys, $$yary[$xhash{$_}]
			foreach (@xs);
	}
#
#	first and last domain values are smallest and biggest now
	$xmin = $xs[0] unless ($is_symbolic || ($xs[0] >= $xmin));
	$xmax = $xs[$#xs] unless ($is_symbolic || ($xs[$#xs] <= $xmax));
	$xmax = 1 + $#xs  if ($is_symbolic && ($#xs >= $xmax));

	foreach (0..$#xs) {
		($x, $y) = ($xs[$_], $ys[$_]); # maybe shift instead ?
		next unless (defined($x) && defined($y));
		
		unless (ref $y) {
			$y = convert_temporal($y, $obj->{timeRange}) if $obj->{timeRange};
#
#	validate the range values
			$obj->{errmsg} = 'Non-numeric range value ' . $y . '.',
			return undef
				unless ($y=~/^[+-]?\.?\d+\.?\d*([Ee][+-]?\d+)?$/);
		
			$obj->{errmsg} = 
				'Invalid value supplied for logarithmic axis.',
			return undef
				if ($obj->{yLog} && ($y <= 0));

			$y = log($y)/log(10) if $obj->{yLog};
			$ymin = $y if ($y < $ymin);
			$ymax = $y if ($y > $ymax);
		}		
		push(@data, $x, $y), next
			unless $obj->{symDomain};
#
#	symbolic domain is mapped according to prior order (if any)
		$domVals->{$x} = defined($domVals) ? scalar(keys(%$domVals)) : 0
			unless defined($domVals->{$x});
		$idx = $domVals->{$x} * 2;
		$data[$idx++] = $x;
		$data[$idx++] = $y;
	}
	push(@{$obj->{data}}, \@data);
	$props .= ' line' unless ($props=~/\bnoline|line|fill\b/i);
	$props=~s/\bnoline\b/line/i if ($props=~/\bfill\b/i);
#	$props .= ' nopoints' 
#		unless (($props=~/\b(no)?points\b/i) || ($props!~/\bline\b/i));
	push(@{$obj->{props}}, $props);
	$obj->{haveScale} = 0;	# invalidate any prior min-max calculations
	$obj->{plotTypes} |= ($props=~/\bfill\b/i) ? AREA : 
		($props=~/\bline\b/i) ? LINE : POINT;
	($obj->{xl}, $obj->{xh}, $obj->{yl}, $obj->{yh}) = 
		($xmin, $xmax, $ymin, $ymax);
	return 1;
}
#
#	wait until plot time to sort domain for bars/histos/candles
#
sub sortData {
	my ($obj) = @_;
#
#	make sure domain values are in ascending order
#
	my $xhash = $obj->{domainValues};
	my @xsorted = ();
	if ($obj->{timeDomain}) {
		@xsorted = sort numerically keys(%$xhash);
	}
	else {
		$xsorted[$$xhash{$_}] = $_
			foreach (keys(%$xhash));
	}
	$obj->{domain} = \@xsorted;
	$obj->{xh} = scalar @xsorted;
	return 1;
}

sub error {
  my $obj = shift;
  return $obj->{errmsg};
}

sub setOptions {
	my ($obj, %hash) = @_;

	foreach (keys (%hash)) {
#
#	we need a lot more error checking here!!!
#
		$obj->{errmsg} = "Unrecognized attribute $_.",
		return undef
			unless ($valid_attr{$_});
		
		if (/^(bg|grid|text)Color$/) {
			$obj->{errmsg} = "Unrecognized color $hash{$_} for $_.",
			return undef
				unless $colors{$hash{$_}};
			my $color = $hash{$_};
#
#	if its a predefined color, reuse it
#	else allocate it
#
			$obj->{$color} = $obj->{img}->colorAllocate(@{$colors{$color}})
				unless $obj->{$color};
			$obj->{$_} = $obj->{$color};
			next;
		}
		
		if ($_ eq 'font') {
#
#	locate first available font and use it
#
			delete $obj->{font};
			foreach my $f (@{$hash{font}}) {
				$obj->{font} = $f, last
					if $obj->loadFont($f);
			}
			$obj->{errmsg} = "No specified font available.",
			return undef
				unless $obj->{font};
			next;
		}

		$obj->{$_} = $hash{$_};
	}
	return 1;
}

sub plot {
	my ($obj, $format) = @_;
	$format = lc $format;
	
	$obj->{errmsg} = 'No plots defined.' unless $obj->{plotTypes};
#
#	first fill with bg color
#
	my $color;
	$obj->{img}->fill(1, 1, $obj->{bgColor} );
	$obj->{img}->rectangle( 0, 0, $obj->{width}-1, $obj->{height}-1,
	 ($obj->{border} ? $obj->{black} : $obj->{bgColor}));
#
#	then add any defined logo
	$obj->addLogo if $obj->{logo};

	$obj->drawTitle if $obj->{title}; # vert offset may be increased
	$obj->drawSignature if $obj->{signature};

#	$obj->{numRanges} = scalar @{$obj->{data}};
	my $rc = 1;
#
#	sort the domain values if temporal domain
#
	$obj->sortData if $obj->{symDomain};

	my $plottypes = $obj->{plotTypes};
	my $props = $obj->{props};
	my $prop;
#
#	if its boxchart only, then establish dummy yl, yh
	($obj->{yl}, $obj->{yh}) = (1, 100) if ($plottypes == BOX);
#
#	get scale of all included plots
#
	$rc = $obj->computeScales()
		unless ($obj->{haveScale} || 
			($plottypes == PIE) || ($plottypes == QUADTREE));
	return undef unless $rc;
#
#	if boxchart included, distribute the range values among the
#	plots
	$obj->{boxHeight} = int($obj->{plotHeight}/($obj->{boxCount}+1))
		if $obj->{boxCount};
#
#	pies are always solo, get em out of the way...
	$rc = $obj->plotPie,
	return ($rc ? (($format) && $obj->{img}->$format) : undef)
		if ($plottypes == PIE);
#
#	plot axes based on plot type
#
	$rc = ($plottypes == BOX) ? $obj->plotBoxAxes :
		($plottypes & (HISTO|GANTT)) ? $obj->plotHistoAxes :
		$obj->plotAxes
		unless ($plottypes == QUADTREE);
	return undef unless $rc;
#
#	now we can plot each dataset
#
	my @proptypes = ();
	foreach (@{$obj->{props}}) {
		push(@proptypes, $typemap{uc $1}), next 
			if /\b(candle|fill|box|bar|histo|gantt|quadtree)\b/i;
		push(@proptypes, POINT),next if /\bnoline\b/i;
		push(@proptypes, LINE);
	}
	my $plotcnt = $#{$obj->{props}} + 1;
#
#	hueristically render plots in "best" visible order
#
	if ($obj->{zAxisLabel} || $obj->{threed}) {
		return undef	# since 3-D only compatible with 3-D 
			if (! $obj->plot3DBars);

		return undef
			unless (($#{$obj->{_legends}} < 0) || $obj->drawLegend);

		$obj->plot3DTicks;
		return (($format) && $obj->{img}->$format);
	}

	return undef	# since quadtree must be solo
		if (($plottypes & QUADTREE) && (! $obj->plotQuadtree(\@proptypes)));

	return undef	# since histo only compatible with histo
		if (($plottypes & HISTO) && (! $obj->plot2DBars(HISTO, \@proptypes)));

	return undef	# since Gantt only compatible with Gantt
		if (($plottypes & GANTT) && (! $obj->plotGantt));

	return undef 
		if (($plottypes & AREA) && (! $obj->plotAll(AREA,\@proptypes)));
		
	return undef
		if (($plottypes & BAR) && (! $obj->plot2DBars(BAR, \@proptypes)));

	return undef
		if (($plottypes & CANDLE) && (! $obj->plot2DBars(CANDLE, \@proptypes)));

	return undef
		if (($plottypes & BOX) && (! $obj->plotBox(\@proptypes)));

	return undef 
		if (($plottypes & LINE) && (! $obj->plotAll(LINE,\@proptypes)));
		
	return undef 
		if (($plottypes & POINT) && (! $obj->plotAll(POINT,\@proptypes)));
#
#	add any accumulated legends
#
	return undef
		unless (($#{$obj->{_legends}} < 0) || $obj->drawLegend);
#
#	now render it in the requested format
#
	return (($format) && $obj->{img}->$format);
}

sub getMap {
	my ($obj) = @_;
	my $mapname = $obj->{genMap};

	return "\$$mapname = [\n" . $obj->{imgMap} . " ];"
		if (uc $obj->{mapType} eq 'PERL');

	return 	"<MAP NAME=\"$mapname\">" . 
		$obj->{imgMap} . "\n</MAP>\n";
}

# 
# sets xscale, yscale, and edge values used in pt2pxl
#	also adjusts min or max of barcharts to clip away origin
#
sub computeScales {
	my $obj = shift;
	my ($xl, $yl, $zl, $xh, $yh, $zh) = 
		($obj->{xl}, $obj->{yl}, $obj->{zl}, $obj->{xh}, $obj->{yh}, 
			$obj->{zh});
	my $i;
#
#	if keepOrigin, make sure (0,0) is included
#	(but only if not in logarithmic mode)
#
	if ($obj->{keepOrigin}) {
		unless ($obj->{xLog} || $obj->{symDomain} ||
			$obj->{zAxisLabel} || $obj->{threed}) {
			$xl = 0 if ($xl > 0);
			$xh = 0 if ($xh < 0);
		}
		unless ($obj->{yLog}) {
			$yl = 0 if ($yl > 0);
			$yh = 0 if ($yh < 0);
		}
#
#	doesn't apply to Z axis (yet)
#
	}
	
	my $plottypes = $obj->{plotTypes};
# set axis ranges for widest/tallest/deepest dataset
	$obj->{errmsg} = 'Invalid dataset.',
	return undef
		unless $obj->computeRanges($xl, $xh, $yl, $yh, $zl, $zh);
	$obj->{yl} = 0 if (($plottypes & (BAR|HISTO)) && ($yl == 0));
	if ($obj->{keepOrigin}) {
		unless ($obj->{xLog} || $obj->{symDomain} ||
			$obj->{zAxisLabel} || $obj->{threed}) {
			$obj->{xl} = 0 if ($xl >= 0);
			$obj->{xh} = 0 if ($xh <= 0);
		}
		unless ($obj->{yLog}) {
			$obj->{yl} = 0 if ($yl >= 0);
			$obj->{yh} = 0 if ($yh <= 0);
		}
	}

	($xl, $xh, $yl, $yh, $zl, $zh) = 
		($obj->{xl}, $obj->{xh}, $obj->{yl}, $obj->{yh}, 
			$obj->{zl}, $obj->{zh});

	if (($plottypes & (BAR|HISTO)) && ($yl > 0) 
		&& (! $obj->{keepOrigin})) {
#
#	adjust mins to clip away from origin
#
		foreach (0..$#{$obj->{props}}) {
			next unless ($obj->{props}->[$_]=~/\b(bar|histo)\b/i);
			my $datastack = $obj->{data}->[$_];
			my $j = 1;

			$datastack->[$j]->[0] = $yl, $j += 3
				while ($j <= $#$datastack);
		}
	}
#
#	heuristically adjust image margins to fit labels
#
	my ($botmargin, $topmargin, $ltmargin, $rtmargin) = (40, 40, 0, 5*$sfw);
	$botmargin += (3 * $tfh) if $obj->{legend};
#
#	compute space needed for X axis labels
#
	my $maxlen = 0;
	my ($tl, $th) = (0, 0);
	($tl, $th) = ($obj->{xLog}) ? (10**$xl, 10**$xh) : ($xl, $xh)
		unless $obj->{symDomain};
	$maxlen = $obj->{symDomain} ? $obj->{xMaxLen} : 
		$obj->{timeDomain} ? length($obj->{timeDomain}) :
		(length($th) > length($tl)) ? length($th) : length($tl);
	$maxlen = 25 if ($maxlen > 25);
	$maxlen = 7 if ($maxlen < 7);
	$botmargin += (($sfw * $maxlen) + 10) unless ($plottypes & (HISTO|GANTT));
	$ltmargin = (($sfw * $maxlen) + 20) if ($plottypes & (HISTO|GANTT));
#
#	compute space needed for Y axis labels
#
	($tl, $th) = ($obj->{yLog}) ? (10**$yl, 10**$yh) : ($yl, $yh);
	$maxlen = $obj->{timeRange} ? length($obj->{timeRange}) :
		(length($th) > length($tl)) ? length($th) : length($tl);
	$maxlen = 25 if ($maxlen > 25);
	$maxlen = 7 if ($maxlen < 7);
	$botmargin += (($sfw * $maxlen) + 10) if ($plottypes & (HISTO|GANTT));
	$ltmargin = (($sfw * $maxlen) + 20) unless ($plottypes & (HISTO|GANTT));
#
#	compute space needed for Z axis labels
#
	if ($obj->{zAxisLabel}) {
		$maxlen = $obj->{zMaxLen};
		$maxlen = 25 if ($maxlen > 25);
		$maxlen = 7 if ($maxlen < 7);
		$rtmargin = ($sfw * $maxlen) + 10;
	}
#
# calculate axis scales 
	if ($obj->{zAxisLabel} || $obj->{threed}) {
		my $tht = $obj->{height} - $topmargin - $botmargin;
		my $twd = $obj->{width} - $ltmargin - $rtmargin;
#
#	compute ratio of Z values to X values
#	to adjust percent of plot area reserved for
#	depth. Max is 40%, min is 10%
#
		my $xzratio = 
			$obj->{Zcard}/($obj->{Xcard}*(scalar @{$obj->{data}}));
#		$xzratio = 0.1 if ($xzratio < 0.1);
#
#	compute actual height as adjusted height x (1 - depth ratio)
#	actual depth is based on 30 deg. rotation of adjusted
#	width x depth ratio
#	actual width is adjust width - the 30 deg. rotation effect
#
		$xh = 0.5 + int($xh), $xl = 0.5,
		$obj->{xh} = $xh, $obj->{xl} = $xl
			if ($xh - $xl < int($xh));
		$obj->{plotWidth} = int($twd / ($xzratio*sin(3.1415926/6) + 1)),
		$obj->{plotDepth} = int(($twd - $obj->{plotWidth})/sin(3.1415926/6)),
		$obj->{plotHeight} = int($tht - ($obj->{plotDepth}*cos(3.1415926/3))),
#		$obj->{xscale} = $obj->{plotWidth}/($xh - $xl),
		$obj->{xscale} = $obj->{plotWidth}/int($xh),
		$obj->{yscale} = $obj->{plotHeight}/($yh - $yl),
		$obj->{zscale} = $obj->{plotDepth}/($zh - $zl)
			unless ($plottypes & (HISTO|GANTT));

		$obj->{plotHeight} = int($tht / ($xzratio*cos(3.1415926/6) + 1)),
		$obj->{plotDepth} = int(($tht - $obj->{plotHeight})/cos(3.1415926/6)),
		$obj->{plotWidth} = int($twd - ($obj->{plotDepth}*sin(3.1415926/6))),
		$obj->{yscale} = $obj->{plotWidth}/($yh - $yl),
		$obj->{xscale} = $obj->{plotHeight}/($xh - $xl),
		$obj->{zscale} = $obj->{plotDepth}/($zh - $zl)
			if ($plottypes & (HISTO|GANTT));
	}
	else {
#	keep true width/height for future reference
		$obj->{xscale} = ($obj->{width} - $ltmargin - $rtmargin)/($xh - $xl),
		$obj->{yscale} = ($obj->{height} - $topmargin - $botmargin)/($yh - $yl),
		$obj->{plotWidth} = $obj->{width} - $ltmargin - $rtmargin,
		$obj->{plotHeight} = $obj->{height} - $topmargin - $botmargin
			unless ($plottypes & (HISTO|GANTT));

		$obj->{yscale} = ($obj->{width} - $ltmargin - $rtmargin)/($yh - $yl),
		$obj->{xscale} = ($obj->{height} - $topmargin - $botmargin)/($xh - $xl),
		$obj->{plotWidth} = $obj->{width} - $ltmargin - $rtmargin,
		$obj->{plotHeight} = $obj->{height} - $topmargin - $botmargin
			if ($plottypes & (HISTO|GANTT));
	}

	$obj->{horizEdge} = $ltmargin;
	$obj->{vertEdge} = $obj->{height} - $botmargin;
#
#	compute spacing info for bar/candles
#
	return undef
		if (($plottypes & (BAR|HISTO)) && 
			(! $obj->{zAxisLabel}) &&
			(! $obj->computeSpacing($plottypes)));
	
	$obj->{haveScale} = 1;
	return 1;
}

# computes the axis ranges for the input (min,max) tuple
# also computes axis step size for ticks
sub computeRanges {
 	my ($obj, $xl, $xh, $yl, $yh, $zl, $zh) = @_;
 	my ($tmp, $om) = (0,0);
 	my @sign = ();

	($obj->{horizStep}, $obj->{vertStep}, $obj->{depthStep}) = (1,1,1),
	($obj->{xl}, $obj->{xh}, $obj->{yl}, $obj->{yh}, $obj->{zl}, $obj->{zh}) = 
		(0,1,0,1, 0,1)
		if (($xl == $xh) || ($yl == $yh) || 
			(defined($zl) && ($zl == $zh)) );
		
	foreach ($xl, $xh, $yl, $yh, $zl, $zh) {
		push @sign, (($_ < 0) ? -1 : (! $_) ? 0 : 1)
			if defined($_);
	}
	$xh = 2 if (($xh == 1) && ($xl == 1));
#
#	tick increment/value algorithm:
#	z = (log(max - min))/log(10);
#	y = z - int(z);
#	scale = (y < 0.4) ? 10 ** (int(z) - 1) :
#		((y >= 0.87) ? 10 ** int(z)) : 5 * ( 10 ** (int(z) - 1));
#	num_of_ticks = int((max - min)/scale) + 2;
#	step_pixels = int(image_width/num_of_ticks)
#
	my ($xr, $xd, $xs);
	$xl = int($xl) - ($xl < 0 ? 1 : 0),
	$xh = int($xh) + 1
		if ($obj->{xLog});
	return undef if ($xh == $xl);
	$xr = (log($xh - $xl))/log(10),
	$xd = $xr - int($xr)
		unless $obj->{symDomain};
	$obj->{horizStep} = $obj->{symDomain} ? 1 : 
		($xd < 0.4) ? (10 ** (int($xr) - 1)) :
		(($xd >= 0.87) ? (10 ** int($xr)) : (5 * (10 ** (int($xr) - 1))));
#
#	align date domain steps on 12:00 AM (zero hour) of a day
#	align timestamps on days if total interval > 3 days
#	else align timestamps on hours if total interval > 3 hours
#	else align timestamps on minutes
#
	if ((! $obj->{symDomain}) && $obj->{timeDomain} 
		&& ($obj->{timeDomain}=~/YYYY/i)) {
		my $align = 
			(($obj->{timeDomain}!~/HH/i) || 
				($xh - $xl > (3 * 24 * 60 * 60))) ? 86400 :
			($xh - $xl > (3 * 60 * 60)) ? 3600 : 60;

		$obj->{horizStep} += ($align - $obj->{horizStep}%$align)
			if ($obj->{horizStep}%$align != 0);
	}
	
	($yl, $yh) = (($yl * 0.75), ($yl * 1.25)) if ($yh == $yl);
	$yl = int($yl) - ($yl < 0 ? 1 : 0),
	$yh = int($yh) + 1
		if ($obj->{yLog});
	return undef if ($yh == $yl);
	$xr = (log($yh - $yl))/log(10);
	$xd = $xr - int($xr);
	$obj->{vertStep} = ($xd < 0.4) ? (10 ** (int($xr) - 1)) :
		(($xd >= 0.87) ? (10 ** int($xr)) : (5 * (10 ** (int($xr) - 1))));
#
#	align time range steps on 12:00 AM (zero hour) of a day
#	if histo/gantt
#
	if (($obj->{plotTypes} & (HISTO|GANTT)) && 
		$obj->{timeRange} && ($obj->{timeRange}=~/^YYYY/i)) {
		my $align = 
		(($obj->{timeRange}!~/HH/i) || 
			($yh - $yl > (3 * 24 * 60 * 60))) ? 86400 :
		($yh - $yl > (3 * 60 * 60)) ? 3600 : 60;

		$obj->{vertStep} += ($align - $obj->{vertStep}%$align)
			if ($obj->{vertStep}%$align != 0);
	}
#
#	histos switch things
	$xs = $obj->{horizStep}, 
	$obj->{horizStep} = $obj->{vertStep}, 
	$obj->{vertStep} = $xs
		if ($obj->{plotTypes} & (HISTO|GANTT));

	if (($obj->{zAxisLabel} || $obj->{threed}) && ($zh != $zl)) {
		return undef if ($zh == $zl);
		$xr = (log($zh - $zl))/log(10),
		$xd = $xr - int($xr)
			unless $obj->{symDomain};
		$obj->{depthStep} = $obj->{symDomain} ? 1 : 
			($xd < 0.4) ? (10 ** (int($xr) - 1)) :
			(($xd >= 0.87) ? (10 ** int($xr)) : (5 * (10 ** (int($xr) - 1))));
	}
	my ($xm, $ym, $zm) = ($obj->{plotTypes} & (HISTO|GANTT)) ?
		($obj->{vertStep}, $obj->{horizStep}, $obj->{depthStep}) :
		($obj->{horizStep}, $obj->{vertStep}, $obj->{depthStep});

	($zl, $zh) = (0.5, 1.5) if ($obj->{symDomain} && defined($zl) && ($zl == $zh));
	($xl, $xh) = (0.5, 1) if ($obj->{symDomain} && ($xl == $xh));
# fudge a little in case limit equals min or max
	$obj->{zl} = ((! $zm) ? 0 : $zm * (int(($zl-0.00001*$sign[4])/$zm) + $sign[4] - 1)),
	$obj->{zh} = ((! $zm) ? 0 : $zm * (int(($zh-0.00001*$sign[5])/$zm) + $sign[5] + 1))
		if defined($zl);
	$obj->{xl} = (! $xm) ? 0 : $xm * (int(($xl-0.00001*$sign[0])/$xm) + $sign[0] - 1);
	$obj->{xh} = (! $xm) ? 0 : $xm * (int(($xh-0.00001*$sign[1])/$xm) + $sign[1] + 1);
#
#	day align here too
	if ((! $obj->{symDomain}) && 
		$obj->{timeDomain} && ($obj->{timeDomain}=~/^YYYY/i)) {
		my $align = 
			(($obj->{timeDomain}!~/HH/i) || 
				($obj->{xh} - $obj->{xl} > (3 * 24 * 60 * 60))) ? 86400 :
			($obj->{xh} - $obj->{xl} > (3 * 60 * 60)) ? 3600 : 60;

		$obj->{xl} = $obj->{xl} - ($obj->{xl}%$align);
		$obj->{xh} += ($align - ($obj->{xh}%$align));
	}

	$obj->{yl} = ($obj->{yLog}) ? $yl : (! $ym) ? 0 : $ym * (int(($yl-0.00001*$sign[2])/$ym) + $sign[2] - 1);
	$obj->{yh} = ($obj->{yLog}) ? $yh : (! $ym) ? 0 : $ym * (int(($yh-0.00001*$sign[3])/$ym) + $sign[3] + 1);
#
#	day align here too
	if ($obj->{timeRange} && ($obj->{timeRange}=~/^YYYY/i)) {
		my $align = 
			(($obj->{timeRange}!~/HH/i) || 
				($obj->{yh} - $obj->{yl} > (3 * 24 * 60 * 60))) ? 86400 :
			($obj->{yh} - $obj->{yl} > (3 * 60 * 60)) ? 3600 : 60;

		$obj->{yl} = $obj->{yl} - ($obj->{yl}%$align);
		$obj->{yh} += ($align - ($obj->{yh}%$align));
	}
	return 1;
}
#
#	compute bar spacing
#
sub computeSpacing {
	my ($obj, $type) = @_;
#
#	compute number of domain values
#
	my $domains = 0;
	$domains = ($obj->{Xcard}) ? 1 : scalar(@{$obj->{domain}});

	my $bars = $obj->{barCount};
	$bars = $obj->{Xcard} if ($obj->{Xcard});
	my $spacer = 10;
	my $width = ($type & HISTO) ? $obj->{plotHeight} : $obj->{plotWidth};
	my $pxlsperdom = int($width/($domains+1)) - $spacer;

	$obj->{errmsg} = 'Insufficient width for number of domain values.',
	return undef
		if ($pxlsperdom < 2);
#
#	compute width of each bar from number of bars per domain value
#
	my $pxlsperbar = int($pxlsperdom/$bars);

	$obj->{errmsg} = 'Insufficient width for number of ranges or values.',
	return undef
		if ($pxlsperbar < 2);

	$obj->{brushWidth} = $pxlsperbar;
	return 1;
}

sub plot2DBars {
	my ($obj, $type, $typeary) = @_;
	my ($i, $j, $k, $x, $n, $ary, $pxl, $pxr, $py, $pyt, $pyb);
	my ($color, $prop, $s, $colorcnt);
	my @barcolors = ();
	my @brushes = ();
	my @markers = ();
	my @props = ();
	my $legend = $obj->{legend};
	my ($xl, $xh, $yl, $yh) = ($obj->{xl}, $obj->{xh}, $obj->{yl}, 
		$obj->{yh});
	my ($brush, $ci, $t);
	my ($useicon, $marker);
	my $img = $obj->{img};
	my $plottypes = $obj->{plotTypes};
	my @tary = ();
	my $bars = $obj->{barCount};
	my $boff = int($obj->{brushWidth}/2);
	my $ttlw = int($bars * $boff);
	my $domain = $obj->{domain};
	my $xhash = $obj->{domainValues};
	my ($prtX,$prtYH,$prtYL);
	my ($iconw, $iconh) = (0,0);
#
#	get indexes of all same type
	foreach (0..$#$typeary) {
		push(@tary, $_)
			if ($$typeary[$_] == $type);
	}

	for ($n = 0; $n <= $#tary; $n++) {
		@barcolors = ();
		@brushes = ();
		@markers = ();
		$marker = undef;
		$color = 'black';
		$k = $tary[$n];
		$ary = $obj->{data}->[$k];
		$t = $obj->{props}->[$k];
		$t=~s/\s+/ /g;
#		$t = lc $t;
		@props = split (' ', $t);
		my $showvals;
		my $stacked = 0;
		foreach (@props) {
#
#	if its iconic, load the icon image
#
			push(@markers,$1),
			push (@barcolors, undef),
			next
				if /^icon:(\S+)$/i;

			$_ = lc $_;
			$showvals = $1, next
				if /^showvalues:(\d+)/;

			$stacked = 1, next
				if ($_ eq 'stack');

			push (@barcolors, $_), 
			push (@markers, undef),
			next
				if (($type != CANDLE) && $colors{$_});
			
			next unless ($type == CANDLE);
#
#	for candlesticks we rely on the DBD::Chart layer to provide
#	sufficient colors and shapes as needed
			push (@barcolors, $_), 
			next
				if ($colors{$_});
#
#	generate pointshape if requested
#
			push(@markers, $_),
			next
				if ($valid_shapes{$_} && ($_ ne 'null'));
		} # end for each property
#
#	allocate each color we're using
		$colorcnt = 0;
		my ($bw, $bh, $bbasew, $bbaseh) = ($plottypes & HISTO) ?
			(1, $obj->{brushWidth}, 0, $obj->{brushWidth}) :
			($obj->{brushWidth}, 1, $obj->{brushWidth}, 0);

		foreach (@barcolors) {
			$colorcnt++;
			push(@brushes, undef),
			next 
				unless $_;
			$obj->{$_} = $obj->{img}->colorAllocate(@{$colors{$_}})
				unless $obj->{$_};
#
#	generate brushes to draw bars
#
			$brush = new GD::Image($bw, $bh),
			$ci = $brush->colorAllocate(@{$colors{$_}}),
			$brush->filledRectangle(0,0,$bbasew, $bbaseh,$ci),
			push(@brushes, $brush);
		}
#
#	load each icon we're using
#
		foreach (0..$#markers) {
			next unless $markers[$_];
			$markers[$_] = ($valid_shapes{$markers[$_]} && ($markers[$_] ne 'null')) ? 
				$obj->make_marker($markers[$_], $barcolors[$_]) : 
				$obj->getIcon($markers[$_], 1);
			return undef unless $markers[$_];
		}
#
#	render legend if requested
#	(a bit confusing here for multicolor single range charts?)
		$obj->addLegend($barcolors[0], $markers[0], $$legend[$k], undef)
			if ((! $stacked) && $legend && $$legend[$k]);

		if ($stacked && $legend && $$legend[$k]) {
#
#	there may be alignment problems here, due to the
#	possibility of drawing multiple stacked bars in the same image
#
			$obj->addLegend($barcolors[$_], $markers[$_], $$legend[$k]->[$_], undef)
				foreach (0..$#{$$legend[$k]});
		}
#
#	heuristically determine whether to print Y values vert or horiz.
		my $yorient = (length($yl) > length($yh)) ? length($yl) : length($yh);
		$yorient *= $tfw;
#
#	compute the center data point, then
#	adjust horizontal location based on brush width
#	and data set number
#
		my $xoffset = ($n * $obj->{brushWidth}) - $ttlw 
			+ $boff;
		my @val_palette = ();
		my ($px, $py);
		$j = 0;
		for ($x = 0; $x <= $#$domain; $x++) {
			($iconw,$iconh) = (0,0);
			$i = $$xhash{$$domain[$x]} * 3;	# get actual index for the current point
			next unless defined($$ary[$i+1]);

# compute top and bottom (left/right) points, and stuff into
#	array with printable x,y and either the brush or marker we use to draw
#	NOTE: this implementation supports stacked and unstacked
			my @ppts = ();
			my $ys = $$ary[$i+1];
			$j = 0 if ($#$ys > 1);
			foreach (0..$#$ys) {
				($pxl, $pyb) = $obj->pt2pxl ( $x+1, $$ys[$_] );
				($pxr, $pyt) = $obj->pt2pxl ( $x+1, $$ys[$_+1] );
				push @ppts, $pxl, $pyb, $pxr, $pyt, $x+1, $$ys[$_], $$ys[$_+1], $j;
				$j++;
				$j = 0 if ($j >= $colorcnt);
				last if ($_+1 == $#$ys);
			}
#
#	render each bar segment
			while ($#ppts > 0) {
				$pxl = shift @ppts;
				$pyb = shift @ppts;
				$pxr = shift @ppts;
				$pyt = shift @ppts;
				$prtX = shift @ppts;
				$prtYL = shift @ppts;
				$prtYH = shift @ppts;
				my $bidx = shift @ppts;
#
#	adjust for bar location
				$pxl += $xoffset,
				$pxr += $xoffset
					unless ($plottypes & HISTO);
				$pyb += $xoffset,
				$pyt += $xoffset
					if ($plottypes & HISTO);
				
# draw line between top and bottom(left and right)
				$img->setBrush($brushes[$bidx]),
				$img->line($pxl, $pyb, $pxr, $pyt, gdBrushed)
					if $brushes[$bidx];
#
#	unless its iconic
#
				$obj->drawIcons($markers[$bidx], $pxl, $pyb, $pxr, $pyt)
					if (($type != CANDLE) && $markers[$bidx]);
#
#	check for shapes if its a CANDLE
#
# draw pointshape if requested
				$obj->drawIcons($markers[$bidx], $pxl, $pyb, $pxl, 0),
				$obj->drawIcons($markers[$bidx], $pxl, $pyt, $pxl, 0),
				($iconw, $iconh) = $markers[$bidx]->getBounds()
					if ($markers[$bidx] && ($type == CANDLE));
#
#	optimization
				next unless ($obj->{genMap} || $showvals);
#
# draw top/bottom values if requested
				if ($type == CANDLE) {
					$prtYH = 10**$prtYH if $obj->{yLog};
					$prtYL = 10**$prtYL if $obj->{yLog};
					$prtYH = restore_temporal($prtYH, $obj->{timeRange}),
					$prtYL = restore_temporal($prtYL, $obj->{timeRange}) 
						if $obj->{timeRange};
					$prtX = restore_temporal($prtX, $obj->{timeDomain}) 
						if $obj->{timeDomain};

# update imagemap if requested
					$obj->updateImagemap('CIRCLE', $prtYH, $k, $prtX, 
						$prtYH, undef, $pxl, $pyt, 4),
					$obj->updateImagemap('CIRCLE', $prtYL, $k, $prtX,
						$prtYL, undef, $pxl, $pyb, 4)
						if ($obj->{genMap});

					next unless ($showvals);
#
#	we need a better way to position the values for stacked candles
#
					$iconh >>= 1;
					$obj->string($showvals, 0, $pxl-(length($prtYL) * ($tfw>>1)),
						$pyb+4+$iconh, $prtYL, $tfw);
					$obj->string($showvals, 0, $pxl-(length($prtYH) * ($tfw>>1)),
						$pyt-$tfh-$iconh, $prtYH, $tfw);
					next;
				}
#
#	convert range/domain values for printing
				$prtYH -= $prtYL if ($stacked && ($prtYL > 0));
				$prtYH = 10**($prtYH) if $obj->{yLog};
				$prtYH = restore_temporal($prtYH, $obj->{timeRange}) 
					if $obj->{timeRange};
				$prtYH = $prtYL if ($prtYL < 0);
				$prtX = restore_temporal($prtX, $obj->{timeDomain}) 
					if $obj->{timeDomain};
#
# update imagemap if requested
				$obj->updateImagemap('RECT', $prtYH, $k, $prtX, 
					$prtYH, undef, $pxl-$boff, $pyt, $pxl+$boff, $pyb)
					if (($plottypes & BAR) && $obj->{genMap});

				$obj->updateImagemap('RECT', $prtYH, $k, $prtX, 
					$prtYH, undef, $pxl, $pyt-$boff, $pxr, $pyt+$boff)
					if (($plottypes & HISTO) && $obj->{genMap});
				
				next unless $showvals;
#
#	draw vertical values for bars
				$py = ($stacked) ?
					(($prtYL < 0) ? $pyb - 4 : 
						$pyt + 4 + int(length($prtYH) * $tfw)) :
					(($prtYL < 0) ? $pyb + 4 + int(length($prtYH) * $tfw) :
						$pyt - 4);
#
#	push info on value stack to draw after rendering bars,
#	since stacked bars may obscure any value we write now
				push(@val_palette, $pxl-int($tfw/2), $py, $prtYH), next
					if (($plottypes & BAR) &&
						($obj->{yLog} || ($yorient >= $obj->{brushWidth})));
#
#	unless they'll fit horiz.
				$py = ($stacked) ? 
					(($prtYL < 0) ? $pyb - $tfh - 4 : $pyt + 4) :
					(($prtYL < 0) ? $pyb + 4 : $pyt - $tfh - 4);
				push(@val_palette, $pxl-int(length($prtYH) * $tfw/2), $py, $prtYH),
				next
					if (($plottypes & BAR) &&
						($yorient < $obj->{brushWidth}));

				$px = ($stacked) ?
					(($prtYL < 0) ? $pxl + $tfw : $pxr - (length($prtYH) * $tfw) - 4) :
					(($prtYL < 0) ? $pxl - (length($prtYH) * $tfw) : $pxr + $tfw);
				push(@val_palette, $px, $pyt-4, $prtYH), next
					if ($plottypes & HISTO);
			} # end while values to plot
		} # end for each bar
#
#	now render values...note that candlesticks still use old method;
#	eventually we'll clean them up as well.
#	We need to compute complementary colors for rendering the text
#	inside bars!!!
#
		while ($#val_palette >= 0) {
			$px = shift @val_palette;
			$py = shift @val_palette;
			$prtYH = shift @val_palette;

			$obj->string($showvals, 90, $px, $py, $prtYH, $tfw), 
			next
				if (($plottypes & BAR) &&
					($obj->{yLog} || ($yorient >= $obj->{brushWidth})));

			$obj->string($showvals, 0, $px, $py, $prtYH, $tfw);
		}
	} # end for each plot
	return 1;
}

sub computeMedian {
	my ($ary, $lo, $hi) = @_;
	my $size = $hi - $lo +1;
	my $midi = $size>>1;
	$midi-- unless ($size & 1);
	$midi += $lo;
	return ($size & 1) ? $$ary[$midi] : (($$ary[$midi] + $$ary[$midi+1])/2);
}

sub computeBox {
	my ($obj, $k) = @_;
	my ($median, $uq, $lq, $lex, $uex, $midpt, $iqr, $val);
	
	my $ary = $obj->{data}->[$k];
	my $size = $#$ary;
#
#	compute median
	$median = computeMedian($ary, 0, $size);
#
#	compute quartiles
	$midpt = ($size)>>1;
	$midpt-- unless ($size & 1);
	$lq = computeMedian($ary, 0, $midpt);
	$midpt += ($size & 1) ? 1 : 2;
	$uq = computeMedian($ary, $midpt, $size);
#
#	compute extremes within 1.5 IQR of median
	$iqr = $uq - $lq;
	$lex = $lq - ($iqr*1.5);
	$uex = $uq + ($iqr*1.5);
	$lex = $$ary[0] if ($lex < $$ary[0]);
	$uex = $$ary[$#$ary] if ($uex > $$ary[$#$ary]);
	
	return ($median, $lq, $uq, $lex, $uex);
}

sub plotBox {
	my ($obj, $typeary) = @_;

	my $legend = $obj->{legend};
	my ($i, $j, $k, $n, $x);
	my @tary = ();

	for ($i = 0; $i <= $#$typeary; $i++) {
		next unless ($$typeary[$i] == BOX);
		push @tary, $i;
	}
#
#	compute the height of each box based range max and min
	my $boxht = ($obj->{yh} - $obj->{yl})/($i+1);

	for ($n = 0; $n <= $#tary; $n++) {
		$k = $tary[$n];

		my $ary = $obj->{data}->[$k];
		my $t = lc $obj->{props}->[$k];
		$t=~s/\s+/ /g;
		my @props = split(' ', $t);
		my $color = 'black';
		my ($val, $xoff);
		my $showvals;
		foreach (@props) {
			$showvals = $1, next if /^showvalues:(\d+)/i;

			$color = $_
				if ($colors{$_});
		}
		$obj->{$color} = $obj->{img}->colorAllocate(@{$colors{$color}})
			unless $obj->{$color};
			
		$obj->addLegend($color, undef, $$legend[$k], undef)
			if (($legend) && ($$legend[$k]));
#
#	compute median, quartiles, and extremes
#
		my ($median, $lq, $uq, $lex, $uex) = $obj->computeBox($k);
#
#	compute box bounds
		my $ytop = $obj->{yl} + ($boxht * ($n + 1));
		my $ybot = $ytop - $boxht;
		my $dumy = ($ytop + $ybot)/2;
		my $py = 0;
#
#	draw the box
		my ($p1x, $p1y) = $obj->pt2pxl($lq, $ytop);
		my ($p2x, $p2y) = $obj->pt2pxl($uq, $ybot);
		my $yoff = (($n+1) * (15 + $tfh));
		$p1y -= $yoff;
		$p2y -= $yoff;
		my $img = $obj->{img};
#
#	double up the box border
		$img->rectangle($p1x, $p1y, $p2x, $p2y, $obj->{$color});
		$img->rectangle($p1x+1, $p1y+1, $p2x-1, $p2y-1, $obj->{$color});

		my ($tmed, $tlex, $tuex) = ($median, $lex, $uex);
		$tmed = restore_temporal($tmed, $obj->{timeDomain}),
		$lq = restore_temporal($lq, $obj->{timeDomain}),
		$uq = restore_temporal($uq, $obj->{timeDomain}) ,
		$tlex = restore_temporal($tlex, $obj->{timeDomain}),
		$tuex = restore_temporal($tuex, $obj->{timeDomain}) 
			if ($obj->{timeDomain} && ($obj->{genMap} || $showvals));

		$xoff = int(length($lq) * $tfw/2),
		$obj->string($showvals,0,$p1x-$xoff,$p1y-$tfh, $lq, $tfw),
		$xoff = int(length($uq) * $tfw/2),
		$obj->string($showvals,0,$p2x-$xoff,$p1y-$tfh, $uq, $tfw)
			if ($showvals);
	
		$obj->updateImagemap('RECT', "$tmed\[$lq..$uq\]", 0, $tmed, 
			$lq, $uq, $p1x, $p1y, $p2x, $p2y)
			if ($obj->{genMap});
#
#	draw median line
		($p1x, $py) = $obj->pt2pxl($median, $dumy);
		$p1y -= 5;
		$p2y += 5;
		$img->line($p1x, $p1y, $p1x, $p2y, $obj->{$color});

		$xoff = int(length($median) * $tfw/2),
		$obj->string($showvals,0,$p1x-$xoff,$p1y-$tfh, $tmed , $tfw)
			if $showvals;
#
#	draw whiskers
		($p1x, $p1y) = $obj->pt2pxl($lex, $dumy);
		($p2x, $py) = $obj->pt2pxl($lq, $dumy);
		$p1y -= $yoff;
		$img->line($p1x, $p1y, $p2x, $p1y, $obj->{$color});

		$tmed = restore_temporal($tmed, $obj->{timeDomain}),
		$lq = restore_temporal($lq, $obj->{timeDomain}),
		$uq = restore_temporal($uq, $obj->{timeDomain}) 
			if ($obj->{timeDomain} && ($obj->{genMap} || $showvals));

		$xoff = int(length($lex) * $tfw/2),
		$obj->string($showvals,0,$p1x-$xoff,$p1y-$tfh, $tlex, $tfw)
			if $showvals;
		$obj->updateImagemap('CIRCLE', $tlex, 0, $tlex, undef, undef, 
			$p1x, $p1y, 4)
			if ($obj->{genMap});

		($p1x, $p1y) = $obj->pt2pxl($uq, $dumy);
		($p2x, $py) = $obj->pt2pxl($uex, $dumy);
		$p1y -= $yoff;
		$img->line($p1x, $p1y, $p2x, $p1y, $obj->{$color});

		$xoff = int(length($uex) * $tfw/2),

		$obj->string($showvals,0,$p2x-$xoff,$p1y-$tfh, $tuex, $tfw)
			if $showvals;
		$obj->updateImagemap('CIRCLE', $tuex, 0, $tuex, undef, undef, 
			$p2x, $p1y, 4)
			if ($obj->{genMap});
#
#	plot outliers; we won't show values here
#	NOTE: we should us pointshape provided by props!!!
#
		my $marker = $obj->make_marker('filldiamond', $color);
		foreach (@$ary) {
			last if ($_ >= $lex);
			($p1x, $p1y) = $obj->pt2pxl($_, $dumy);
			$p1y -= $yoff;
			$img->copy($marker, $p1x-4, $p1y-4, 0, 0, 9, 9);
		}
		for (my $i = $#$ary; ($i > 0) && ($uex < $$ary[$i]); $i--) {
			($p1x, $p1y) = $obj->pt2pxl($$ary[$i], $dumy);
			$p1y -= $yoff;
			$img->copy($marker, $p1x-4, $p1y-4, 0, 0, 9, 9);
		}
	}	# end for each box plot
	return 1;
}

sub plotBoxAxes {
	my $obj = shift;
	my ($p1x, $p1y, $p2x, $p2y);
	my $img = $obj->{img};
	my ($xl, $xh, $yl, $yh) = ($obj->{xl}, $obj->{xh}, 
		$obj->{yl}, $obj->{yh});

	my $yaxpt = ((! $obj->{yLog}) && ($yl < 0) && ($yh > 0)) ? 0 : $yl;
	my $xaxpt = ((! $obj->{xLog}) && ($xl < 0) && ($xh > 0)) ? 0 : $xl;
#
#	X axis
	($p1x, $p1y) = $obj->pt2pxl($xl, $yaxpt);
	($p2x, $p2y) = $obj->pt2pxl($xh, $yaxpt);
	$img->line($p1x, $p1y, $p2x, $p2y, $obj->{gridColor});
#
#	draw X axis label
	my ($len, $xStart);
	($p2x, $p2y) = $obj->pt2pxl($xh, $yl),
	$len = $sfw * length($obj->{xAxisLabel}),
	$xStart = ($p2x+$len/2 > $obj->{width}-10)
		? ($obj->{width}-10-$len) : ($p2x-$len/2),
	$obj->string(6, 0, $xStart, $p2y+ int(4*$sfh/3), $obj->{xAxisLabel}, $sfw)
		if ($obj->{xAxisLabel});
#
# draw ticks and labels
# 
	my ($i,$px,$py);
#
#	for LOG(X):
#
	my $powk;
	if ($obj->{xLog}) {
		$i = $xl;
		my $n = 0;
		my $k = $i;
		while ($i < $xh) {
			$k = $i + $logsteps[$n++];

			($px,$py) = $obj->pt2pxl($k, $yl);
			($p1x, $p1y) = ($obj->{vertGrid}) ? 
				$obj->pt2pxl($k, $yh) : ($px, $py+2);
			$img->line($px, ($obj->{vertGrid} ? $py : $py-2), 
				$px, $p1y, $obj->{gridColor});

			$powk = ($obj->{timeDomain}) ? 
				restore_temporal(10**$k, $obj->{timeDomain}) : 10**$k,
			$obj->string(6, 90, $px-$sfh/2, $py+length($powk)*$sfw, $powk, $sfw)
				if (($n == 1) && ($px+$sfh < $xStart));

			($n, $i)  = (0, $k)
				if ($n > $#logsteps);
		}
		return 1;
	}

    my $step = $obj->{horizStep}; 
   	my $prtX;
	for ($i = $xl; $i <= $xh; $i += $step ) {
		($px,$py) = $obj->pt2pxl($i, 
			((($obj->{yLog}) || 
			($obj->{vertGrid}) || ($yl > 0) || ($yh < 0)) ? $yl : 0));
		($p1x, $p1y) = ($obj->{vertGrid}) ? 
			$obj->pt2pxl($i, $yh) : ($px, $py+2);
		$img->line($px, ($obj->{vertGrid} ? $py : $py-2), $px, $p1y, $obj->{gridColor});

		next if ($obj->{xAxisVert} && ($px+$sfh >= $xStart));
		$prtX = $obj->{timeDomain} ? restore_temporal($i, $obj->{timeDomain}) : $i;
		$obj->string(6, 90, $px-($sfh>>1), $py+2+length($prtX)*$sfw, $prtX, $sfw), next
			if ($obj->{xAxisVert});

		$obj->string(6, 0, $px-length($prtX)*($sfw>>1), $py+($sfh>>1), $prtX, $sfw);
	}
	return 1;
}

sub plotAll {
	my ($obj, $type, $typeary) = @_;
	my ($i, $n, $k);
	my @tary = ();
	
	foreach (0..$#$typeary) {
		push(@tary, $_) 
			if ($$typeary[$_] == $type);
	}

	foreach $n (@tary) {
		my $ary = $obj->{data}->[$n];
		my $t = $obj->{props}->[$n];
		$t=~s/\s+/ /g;
#		$t = lc $t;
		my @props = split (' ', $t);
		my $color = 'black';
		my $marker = undef;
		my $line = 'line';
		my @areacolors = ();
		my $stacked = 0;
		my $coloridx = 0;
		my $legend;
		my $lwidth = 1;
		my $anchor = 1;
		my $showvals = 0;
		foreach (@props) {
#
#	if its iconic, load the icon image
#
			$marker = $1,
			next
				if /^icon:(\S+)/i;

			$_ = lc $_;
			push(@areacolors, $_), next
				if ($colors{$_});

			$stacked = 1, next
				if ($_ eq 'stack');

			$showvals = $1, next
				if /^showvalues:(\d+)/i;

			$marker = $_,
			next
				if ($valid_shapes{$_} && ($_ ne 'null'));

			$marker = 'fillcircle',
			next
				if ((! $marker) && ($_ eq 'points'));
					
			$marker = undef, next 
				if ($_ eq 'nopoints');

			$line = $_, next
				if /^(line|noline|fill)$/;

			$lwidth = $1, next if /width:(\d+)/;
			
			$anchor = undef if ($_ eq 'float');
		}

		if (($line eq 'fill') && $stacked) {
#
#	pull apart the datapoint arrays and plot them individually from the top
#	to the bottom
			my @newary = ();
			my $j = $#{$ary->[1]};
#
#	in case our color list is short
			my $k = 0;
			my $colorcnt = @areacolors;
			while ($#areacolors < $j) {
				push @areacolors, $areacolors[$k];
				$k++;
				$k = 0 if ($k == $colorcnt);
			}
			my $looplim = ($anchor ? 0 : 1);
			my $ylo = $obj->{yl};
			$ylo = 0 if ($ylo < 0);
			for (; $j >= $looplim; $j--) {
				@newary = ();
				$color = $areacolors[$j-$looplim];
				$i = 0;
				push(@newary, $$ary[$i], ($anchor ? $ylo : $ary->[$i+1]->[0]), $ary->[$i+1]->[$j]),
				$i += 2
					while ($i <= $#$ary);
				$legend = $obj->{legend} ? $obj->{legend}->[$n]->[$j] : undef;
				return undef unless $obj->plotData($n, \@newary, $color, 'fill', $marker,
					$legend, $lwidth, $anchor, $showvals);
			}
			next;
		}

		$legend = $obj->{legend} ? $obj->{legend}->[$n] : undef;

		if (($line eq 'fill') && $anchor) {
#
#	if its anchored, then add the origin points
#
			my @newary = ();
			$i = 0;
			my $yl = $obj->{yl};
			my $yh = $obj->{yh};
			my $yaxpt = ((! $obj->{yLog}) && ($yl < 0) && ($yh > 0)) ? 0 : $yl;

			push(@newary, $$ary[$i], $yaxpt, $$ary[$i+1]), $i += 2
				while ($i <= $#$ary);

			return undef unless $obj->plotData($n, \@newary, 
				$areacolors[$coloridx], $line, $marker,
				$obj->{legend}->[$n], $lwidth, $anchor, $showvals);
		}
		else {
			return undef unless $obj->plotData($n, $ary, $areacolors[$coloridx], $line, $marker,
				$obj->{legend}->[$n], $lwidth, $anchor, $showvals);
		}
		$coloridx++;
		$coloridx = 0 if ($coloridx = $#areacolors);
	}
	return 1;
}

# draws the specified dataset in $obj->{data}
sub plotData {
	my ($obj, $k, $ary, $color, $line, $marker, $legend, $lw, $anchor, $showvals) = @_;
	my ($i, $n, $px, $py, $prevpx, $prevpy, $pyt, $pyb);
	my ($img, $prop, $s, $voff);
	my @props = ();
# legend is left justified underneath
	my ($xl, $xh, $yl, $yh) = ($obj->{xl}, $obj->{xh}, $obj->{yl}, 
		$obj->{yh});
	my ($markw, $markh, $yoff, $wdelta, $hdelta);
	$img = $obj->{img};	

	$color = 'black' unless $color;
	$obj->{$color} = $obj->{img}->colorAllocate(@{$colors{$color}})
		unless $obj->{$color};
		
	if ($marker) {
		$marker = ($valid_shapes{$marker} && ($marker ne 'null')) ? 
			$obj->make_marker($marker, $color) :
			$obj->getIcon($marker);
		return undef unless $marker;
		($markw, $markh) = $marker->getBounds();
		$wdelta = $markw>>1;
		$hdelta = $markh>>1;
	}
	$yoff = ($marker) ? $markh : 2;
#
#	render legend if requested
#
	$obj->addLegend($color, $marker, $legend, ($line eq 'line')) if $legend;
#
#	line/point/area charts
#
#	we need to heuristically sort data sets to optimize the view of 
#	overlapping areagraphs...for now the user will need to be smart 
#	about the order of registering the datasets
#
	$obj->fill_region($obj->{$color}, $ary, $anchor)
		if ($line eq 'fill');

	($prevpx, $prevpy) = (0,0);
	my ($prtX, $prtY);

# draw the rest of the points and lines 
	my $domain = $obj->{symDomain} ? $obj->{domain} : $ary;
	my $xhash = $obj->{symDomain} ? $obj->{domainValues} : undef;
	my $domsize = $obj->{symDomain} ? $#$domain : $#$ary;
	my $x;
	my $incr = $obj->{symDomain} ? 1 : ($line eq 'fill') ? 3 : 2;
	my $offset = ($line eq 'fill') ? 2 : 1;
	my $xd;
#
#	create a brush to draw linegraphs
	my $lbrush;
	if ($line eq 'line') {
		$lbrush = new GD::Image($lw,$lw);
		my $ci = $lbrush->colorAllocate(@{$colors{$color}});
		$lbrush->filledRectangle(0,0,$lw, $lw,$ci);
		$img->setBrush($lbrush);
	}

	for ($x = 0; $x <= $domsize; $x += $incr) {
		$xd = $$xhash{$$domain[$x]} if $obj->{symDomain};
		$i = $obj->{symDomain} ? $xd * 2: $x;
		next unless defined($$ary[$i+1]);

# get next point
		($px, $py) = $obj->pt2pxl(($obj->{symDomain} ? $xd+1 : $$ary[$i]),
			$$ary[$i+$offset] );

# draw line from previous point, maybe
		$img->line($prevpx, $prevpy, $px, $py, gdBrushed)
			if (($line eq 'line') && $i);
		($prevpx, $prevpy) = ($px, $py);

# draw point, maybe
		$img->copy($marker, $px-$wdelta, $py-$hdelta, 0, 0, $markw, 
			$markh)
			if ($marker);

		if ($obj->{genMap} || $showvals) {
			($prtX, $prtY) = ($$ary[$i], $$ary[$i+$offset]);
			$prtY = 10**$prtY if $obj->{yLog};
			$prtX = 10**$prtX if $obj->{xLog};
			$prtY = restore_temporal($prtY, $obj->{timeRange}) if $obj->{timeRange};
			$prtX = restore_temporal($prtX, $obj->{timeDomain}) if $obj->{timeDomain};
			$s = $obj->{symDomain} ? $prtY : "($prtX,$prtY)";
		}
			
		$obj->updateImagemap('CIRCLE', $s, $k, $prtX, $prtY,
			undef, $px, $py, 4)
			if ($obj->{genMap});

		$voff = (length($s) * $tfw)>>1,
		$obj->string($showvals,0,$px-$voff,$py-$yoff, $s, $tfw)
			if $showvals;
	}
	return 1;
}

sub addLegend {
	my ($obj, $color, $shape, $text, $line) = @_;
#
#	add the dataset to the legend
#
	push @{$obj->{_legends}}, [ $color, $shape, $text, $line ] ;
	return 1;
}

sub drawLegend {
	my ($obj) = @_;
#
#	add the dataset to the legend using current color
#	and shape (if any)
#
	my ($color, $shape, $text, $line, $props);
	my $legary = $obj->{_legends};

	my $xadj = 30;
	my $xoff = $obj->{horizEdge};
	my $maxyoff = $obj->{height} - 40;
	my $yoff = $obj->{height} - 40 - 20 - (2 * $tfh);
	my ($w, $h);

	while (@$legary) {
		$props = shift @$legary;
		($color, $shape, $text, $line) = @$props;
		
		$color = 'black' unless $color;
		$shape = $obj->make_marker('fillsquare', $color)
			unless ($shape || $line);
#
#	move to next column if shape too big to fit
#
		($w, $h) = $shape ? $shape->getBounds() : (20, int($tfh * 1.5));

		$yoff = $obj->{height} - 40 - 20 - (2 * $tfh),
		$xoff += $xadj
			if ($yoff + $h > $maxyoff);

		$xadj = ((($w < 20) ? 20 : $w) + ($tfw * (length($text)+2)))
			if ($xadj < ((($w < 20) ? 20 : $w) + ($tfw * (length($text)+2))));
		
		my $img = $obj->{img};
		$img->line($xoff, $yoff+4, $xoff+20, $yoff+4, $obj->{$color})
			if $line;

		$obj->string(5, 0,$xoff + ($line ? 25 : ($w + 5)),$yoff, $text, $tfw);

		$img->copy($shape, $xoff+5, $yoff, 0, 0, $w-1, $w-1)
			if $shape;
		
		$yoff += ($h < int($tfh * 1.5)) ? int($tfh * 1.5) : $h;
	}
	return 1;
}

# compute pixel coordinates from datapoint
sub pt2pxl {
	my ($obj, $x, $y, $z) = @_;
	my $plottype = $obj->{plotTypes} & (HISTO|GANTT);

	return (
		int($obj->{horizEdge} + ($x - $obj->{xl}) * $obj->{xscale}),
		int($obj->{vertEdge} - ($y - $obj->{yl}) * $obj->{yscale})
	 ) unless (defined($z) || $plottype);
#
#	histo version
	return (
		int($obj->{horizEdge} + ($y - $obj->{yl}) * $obj->{yscale}),
		int($obj->{vertEdge} - ($x - $obj->{xl}) * $obj->{xscale})
	 ) unless defined($z);
#
#	translate x,y,z into x,y
#
	my $tx = ($x - $obj->{xl}) * $obj->{xscale};
	my $ty = ($y - $obj->{yl}) * $obj->{yscale};
	my $tz = ($z - $obj->{zl}) * $obj->{zscale};

	return
		$obj->{horizEdge} + int($tx + ($tz * 0.433)),
		$obj->{vertEdge} - int($ty + ($tz * 0.25))
		unless $plottype;
#
#	histo version
	return
		$obj->{horizEdge} + int($ty + ($tz * 0.433)),
		$obj->{vertEdge} - int($tx + ($tz * 0.25));
}
# draw the axes, labels, title, grid/ticks and tick labels

sub plotAxes {
	my $obj = shift;
	return $obj->plot3DAxes
		if ($obj->{zAxisLabel} || $obj->{threed});

	my ($p1x, $p1y, $p2x, $p2y);
	my $img = $obj->{img};
	my ($xl, $xh, $yl, $yh) = ($obj->{xl}, $obj->{xh}, 
		$obj->{yl}, $obj->{yh});

	my $yaxpt = ((! $obj->{yLog}) && ($yl < 0) && ($yh > 0)) ? 0 : $yl;
	my $xaxpt = ((! $obj->{xLog}) && ($xl < 0) && ($xh > 0)) ? 0 : $xl;
	my $plottypes = $obj->{plotTypes};
	
	if ($obj->{vertGrid} || $obj->{horizGrid}) {
#
#	gridded, create a rectangle
#
		($p1x, $p1y) = $obj->pt2pxl ($xl, $yl);
		($p2x, $p2y) = $obj->pt2pxl ($xh, $yh);

  		$img->rectangle( $p1x, $p1y, $p2x, $p2y, $obj->{gridColor});
#
#	hilight the (0,0) axes, if available
#
#	draw X-axis
		($p1x, $p1y) = $obj->pt2pxl($xl, $yaxpt);
		($p2x, $p2y) = $obj->pt2pxl($xh, $yaxpt);
		$img->filledRectangle($p1x, $p1y-1,$p2x, $p2y-1,$obj->{gridColor}); # wide line
#	draw Y-axis
		($p1x, $p1y) = $obj->pt2pxl($xaxpt, $yl);
		($p2x, $p2y) = $obj->pt2pxl($xaxpt, $yh);
		$img->filledRectangle($p1x-1, $p2y,$p2x+1, $p1y,$obj->{gridColor}); # wide line
  	}
  	else {
#
#	X axis
		($p1x, $p1y) = $obj->pt2pxl($xl, $yaxpt);
		($p2x, $p2y) = $obj->pt2pxl($xh, $yaxpt);
		$img->line($p1x, $p1y, $p2x, $p2y, $obj->{gridColor});
#
#	draw at bottom if yl < 0
		($p1x, $p1y) = $obj->pt2pxl($xl, $yl),
		($p2x, $p2y) = $obj->pt2pxl($xh, $yl),
		$img->line($p1x, $p1y, $p2x, $p2y, $obj->{gridColor})
			if ($yl < 0);
	}
#
#	draw X axis label
	my ($len, $xStart, $xStart2);
	($p2x, $p2y) = $obj->pt2pxl($xh, $yl);
#		$obj->{vertGrid} || $obj->{horizGrid}) ? $yl : $yaxpt),
	$len = $sfw * length($obj->{xAxisLabel}),
	$xStart = ($p2x+$len/2 > $obj->{width}-10)
		? ($obj->{width}-10-$len) : ($p2x-$len/2),
	$obj->string(6,0, $xStart, $p2y+ int(4*$sfh/3), $obj->{xAxisLabel}, $sfw)
		if ($obj->{xAxisLabel});

# Y axis
	($p1x, $p1y) = $obj->pt2pxl($xaxpt, $yl);
	($p2x, $p2y) = $obj->pt2pxl((($obj->{vertGrid}) ? $xl : $xaxpt), $yh);
	
	$img->line($p1x, $p1y, $p2x, $p2y, $obj->{gridColor})
		if ((! $obj->{'vertGrid'}) && (! $obj->{horizGrid}));

	$xStart2 = $p2x - length($obj->{yAxisLabel}) * ($sfw >> 1),
	$obj->string(6,0, ($xStart2 > 10 ? $xStart2 : 10), 
		$p2y - 3*($sfh>>1), $obj->{yAxisLabel}, $sfw)
		if ($obj->{yAxisLabel});
#
# draw ticks and labels
# 
	my ($i,$px,$py, $step, $j, $txt);
   	my $prevx = 0;
# 
# horizontal
#
#	for LOG(X):
#
	my $powk;
	if ($obj->{xLog}) {
		$i = $xl;
		my $n = 0;
		my $k = $i;
		while ($i < $xh) {
			$k = $i + $logsteps[$n++];

			($px,$py) = $obj->pt2pxl($k, $yl);
			($p1x, $p1y) = ($obj->{vertGrid}) ? 
				$obj->pt2pxl($k, $yh) : ($px, $py+2);
			$img->line($px, ($obj->{vertGrid} ? $py : $py-2), 
				$px, $p1y, $obj->{gridColor});
#
#	don't draw tick labels if we're overwriting the axis label
#
			$powk = ($obj->{timeDomain}) ? 
				restore_temporal(10**$k, $obj->{timeDomain}) : 10**$k,
			$obj->string(6, 90, $px-$sfh/2, 
				$py+length($powk)*$sfw, $powk)
				if (($n == 1) && ($px+$sfh < $xStart));

			($n, $i)  = (0 , $k)
				if ($n > $#logsteps);
		}
	}
	elsif ($obj->{symDomain}) {
#
# symbolic domain
#
		my $ary = $obj->{domain};
    
		for ($i = 1, $j = 0; $i < $xh; $i++, $j++ ) {
			($px,$py) = $obj->pt2pxl($i, $yl);
			($p1x, $p1y) = ($obj->{vertGrid}) ? 
				$obj->pt2pxl($i, $yh) : ($px, $py+2);
			$img->line($px, ($obj->{vertGrid} ? $py : $py-2), 
				$px, $p1y, $obj->{gridColor});
#
#	skip the label if it would overlap
#
			next if ($obj->{xAxisVert} && ($sfh+1 > ($px - $prevx)));
#
#	truncate long labels
#
			$txt = ($obj->{timeDomain}) ? 
				restore_temporal($$ary[$j], $obj->{timeDomain}) : $$ary[$j];
			$txt = substr($txt, 0, 22) . '...' 
				if (length($txt) > 25);

			if ($obj->{xAxisVert}) {
				$prevx = $px;
				next if ($px+$sfh >= $xStart);
				$obj->string(6, 90, $px-($sfh>>1), 
					$py+2+length($txt)*$sfw, $txt, $sfw);
				next;
			}

			next if (((length($txt)+1) * $sfw) > ($px - $prevx));
			$prevx = $px;

			$obj->string(6,0, $px-length($txt)*($sfw>>1), 
				$py+($sfh>>1), $txt, $sfw);
		}
	}
	else {
	    $step = $obj->{horizStep}; 
		for ($i = $xl; $i <= $xh; $i += $step ) {
			($px,$py) = $obj->pt2pxl($i, $yl);
			($p1x, $p1y) = ($obj->{vertGrid}) ? 
				$obj->pt2pxl($i, $yh) : ($px, $py+2);
			$img->line($px, ($obj->{vertGrid} ? $py : $py-2), 
				$px, $p1y, $obj->{gridColor});

			$txt = ($obj->{timeDomain}) ? 
				restore_temporal($i, $obj->{timeDomain}) : $i;
			$txt = substr($txt, 0, 22) . '...' 
				if (length($txt) > 25);

			next if ((! $obj->{xAxisVert}) && 
				($px - $prevx < (length($txt) * $sfw)));
			next if ($obj->{xAxisVert} && ($px - $prevx < $sfw));
			$prevx = $px;
			next if ($obj->{xAxisVert} &&  ($px+$sfh >= $xStart));
			
			$obj->string(6, 90, $px-($sfh>>1), 
				$py+2+length($txt)*$sfw, $txt, $sfw),
			next
				if ($obj->{xAxisVert});

			$obj->string(6, 0,$px-length($txt)*($sfw>>1), 
				$py+($sfh>>1), $txt, $sfw);
		}
	}
#
# vertical
#
#	for LOG(Y):
#
	if ($obj->{yLog}) {
		$i = $yl;
		my $n = 0;
		my $k = $yl;
		while ($k < $yh) {
			($px,$py) = $obj->pt2pxl(
				((($obj->{xLog}) || ($obj->{horizGrid})) ? 
				$xl : $xaxpt), $k);
			($p1x, $p1y) = ($obj->{horizGrid}) ? 
				$obj->pt2pxl($xh, $k) : ($px+2, $py);
			$img->line(($obj->{horizGrid} ? $px : $px-2), $py, 
				$p1x, $py, $obj->{gridColor});

			$powk = ($obj->{timeRange}) ? 
				restore_temporal(10**$k, $obj->{timeRange}) : 10**$k,
			$obj->string(6, 0, $px-5-length($powk)*$sfw, 
				$py-($sfh>>1), $powk, $sfw)
				if ($n == 0);
			
			$k = $i + $logsteps[$n++];
			($n, $i) = (0, $k)
				if ($n > $#logsteps);
		}
		return 1;
	}

	$step = $obj->{vertStep};
#
#	if y tick step < (2 * sfh), skip every other label
#
	($px,$py) = $obj->pt2pxl((($obj->{horizGrid}) ? $xl : $xaxpt), $yl);
	($p1x,$p1y) = $obj->pt2pxl((($obj->{horizGrid}) ? $xl : $xaxpt), 
		$yl+$step);
	my $skip = ($p1y - $py < ($sfh<<1)) ? 1 : 0;
	my $tickv = $yl;
	for ($i=0, $j = 0; $tickv < $yh; $i++, $j++ ) {
		$tickv = $yl + ($i * $step);
		last if ($tickv > $yh);
		($px,$py) = $obj->pt2pxl((($obj->{horizGrid}) ? $xl : $xaxpt), $tickv);
		($p1x, $p1y) = ($obj->{horizGrid}) ? 
			$obj->pt2pxl($xh, $tickv) : ($px+2, $py);
		$img->line(($obj->{horizGrid} ? $px : $px-2), $py, $p1x, $py, 
			$obj->{gridColor});

		next if (($skip) && ($j&1));
		$txt = $obj->{timeRange} ? restore_temporal($tickv, $obj->{timeRange}) : $tickv,
		$obj->string(6,0, $px-5-length($txt)*$sfw, $py-($sfh>>1), $txt, $sfw);
	}
	return 1;
}

sub plotHistoAxes {
	my ($obj) = @_;
	return $obj->plot3DAxes
		if ($obj->{zAxisLabel} || $obj->{threed});

	my ($p1x, $p1y, $p2x, $p2y);
	my $img = $obj->{img};
	my ($xl, $xh, $yl, $yh) = ($obj->{xl}, $obj->{xh}, 
		$obj->{yl}, $obj->{yh});
	my $plottypes = $obj->{plotTypes};
#
#	draw horizontal and vertical axes
	($p1x, $p1y) = $obj->pt2pxl ($xl, $yl),
	($p2x, $p2y) = $obj->pt2pxl($xh, $yl),
	$img->line($p1x, $p1y, $p2x, $p2y, $obj->{gridColor}),
	($p2x, $p2y) = $obj->pt2pxl ($xl, $yh),
	$img->line($p1x, $p1y, $p2x, $p2y, $obj->{gridColor})
		unless ($obj->{vertGrid} || $obj->{horizGrid});

	if ($obj->{vertGrid} || $obj->{horizGrid}) {
#
#	hilight the (0,0) axes, if available
#
#	draw horizontal axis
		($p1x, $p1y) = $obj->pt2pxl($xl, $yl);
		($p2x, $p2y) = $obj->pt2pxl($xl, $yh);
		$img->filledRectangle($p1x, $p1y-1, $p2x, $p2y-1,$obj->{gridColor}); # wide line
#	draw vertical axis
		($p1x, $p1y) = $obj->pt2pxl($xl, $yl);
		($p2x, $p2y) = $obj->pt2pxl($xh, $yl);
		$img->filledRectangle($p1x-1, $p2y, $p2x+1, $p1y,$obj->{gridColor}); # wide line

		($p1x, $p1y) = $obj->pt2pxl($xl, 0),
		($p2x, $p2y) = $obj->pt2pxl($xh, 0),
		$img->filledRectangle($p1x-1, $p2y, $p2x+1, $p1y,$obj->{gridColor})
			if (($yl < 0) && ($yh > 0));
#
#	gridded, create a rectangle
#
		($p1x, $p1y) = $obj->pt2pxl ($xh, $yl);
		($p2x, $p2y) = $obj->pt2pxl ($xl, $yh);
  		$img->rectangle( $p1x, $p1y, $p2x, $p2y, $obj->{gridColor});
  	}
#
#	draw horizontal axis label
	my ($len, $xStart, $xStart2);
	$len = $sfw * length($obj->{yAxisLabel}),
	$xStart = ($p2x+$len/2 > $obj->{width}-10) ? 
		($obj->{width}-10-$len) : ($p2x-$len/2),
	$obj->string(6,0, $xStart, $p2y+ int(4*$sfh/3), $obj->{yAxisLabel}, $sfw)
		if ($obj->{yAxisLabel});

# vertical axis label
	($p2x, $p2y) = $obj->pt2pxl($xh, $yl),
	$xStart2 = $p2x - ((length($obj->{xAxisLabel}) * $sfw) >> 1),
	$obj->string(6, 0,($xStart2 > 10 ? $xStart2 : 10), 
		$p2y - 3*($sfh>>1), $obj->{xAxisLabel}, $sfw)
		if $obj->{xAxisLabel};
#
# draw ticks and labels
# 
	my ($i,$px,$py, $step, $j, $txt);
# 
# vertical symbolic domain
#
	my $ary = $obj->{domain};
    
	my $prevx = $obj->{vertEdge};
	for ($i = 1, $j = 0; $i < $xh; $i++, $j++) {
		($px,$py) = $obj->pt2pxl($i, $yl);
		($p1x, $p1y) = ($obj->{horizGrid}) ? 
			$obj->pt2pxl($i, $yh) : ($px+2, $py);
		$img->line(($obj->{horizGrid} ? $px : $px-2), $py, 
			$p1x, $py, $obj->{gridColor});
#
#	skip the label if undefined or it would overlap or its Gantt
#
		next unless (($plottypes & HISTO) && defined($$ary[$j]) && ($sfh < ($prevx - $py)));
		$prevx = $py;
		$txt = ($obj->{timeDomain}) ? 
			restore_temporal($$ary[$j], $obj->{timeDomain}) : $$ary[$j];
#
#	truncate long labels
#
		$txt = substr($txt, 0, 22) . '...' 
			if (length($txt) > 25);

		$obj->string(6, 0, ($px-(length($txt)*$sfw)-5), 
			$py-($sfh>>1), $txt, $sfw);
	}
#
# horizontal
#
#	for LOG(Y):
#
	$prevx = 0;
	if ($obj->{yLog}) {
		$i = $yl;
		my $n = 0;
		my $k = $i;
		my $powk;
		while ($i < $yh) {
			$k = $i + $logsteps[$n++];
			($px,$py) = $obj->pt2pxl($xl, $k);
			($p1x, $p1y) = ($obj->{vertGrid}) ? 
				$obj->pt2pxl($xh, $k) : ($px, $py+2);
			$img->line($px, ($obj->{vertGrid} ? $py : $py-2),
				$px, $p1y, $obj->{gridColor});
#
#	skip the label if it would overlap
#
			next if ($obj->{xAxisVert} && ($sfh > ($px - $prevx)));

			$powk = ($obj->{timeRange}) ? 
				restore_temporal(10**$k, $obj->{timeRange}) : 10**$k;

			($n, $i)  = (0, $k)
				if ($n > $#logsteps);

			next if (length($powk) * ($sfw>>1) > ($px - $prevx));
			next unless ($n == 1);

			$prevx = $px;

			next if ($obj->{xAxisVert} && ($px+$sfh >= $xStart));
			$obj->string(6, 90, $px-($sfh>>1), 
				$py+2+length($powk)*$sfw, $powk, $sfw),
			next
				if $obj->{xAxisVert};

			$obj->string(6, 0, $px-(length($powk) * ($sfw>>1)),
				$py+4, $powk, $sfw);
		}
		return 1;
	}

	$step = $obj->{horizStep};
	for ($i=$yl, $j = 0; $i <= $yh; $i+=$step, $j++ ) {
		($px,$py) = $obj->pt2pxl($xl, $i);
		($p1x,$p1y) = ($obj->{vertGrid}) ? $obj->pt2pxl($xh, $i) : ($px, $py+2);
		$img->line($px, ($obj->{vertGrid} ? $py : $py-2), $px, $p1y, $obj->{gridColor});
		next if ($obj->{xAxisVert} && ($px - $prevx < $sfh+3));

		$txt = $obj->{timeRange} ? restore_temporal($i, $obj->{timeRange}) : $i;
		next unless ($obj->{xAxisVert} || 
			(length($txt) * ($sfw>>1) < ($px - $prevx)));
		$prevx = $px;

		next if ($obj->{xAxisVert} && ($px+$sfh >= $xStart));
		$obj->string(6, 90, $px-($sfh>>1), 
			$py+2+length($txt)*$sfw, $txt, $sfw),
		next
			if $obj->{xAxisVert};

		$obj->string(6,0, $px-(length($txt) * ($sfw>>1)),
			$py+4, $txt, $sfw);
	}
	return 1;
}

sub drawTitle {
	my ($obj) = @_;
	my ($w,$h) = (gdMediumBoldFont->width, gdMediumBoldFont->height);

# centered below chart
	my ($px,$py) = ($obj->{width}/2, $obj->{height} - 40 + $h);

	($px,$py) = ($px - length ($obj->{title}) * $w/2, $py-$h/2);
	$obj->string(7, 0, $px, $py, $obj->{title}, $w); 
}

sub drawSignature {
	my ($obj) = @_;
	my $fw = ($tfw * length($obj->{signature})) + 5;
# in lower right corner
	my ($px,$py) = ($obj->{width} - $fw, $obj->{height} - ($tfh * 2));

	$obj->string(5, 0, $px, $py, $obj->{signature}, $tfw); 
}

sub fill_region {
	my ($obj, $ci, $ary, $anchor) = @_;
	my $img = $obj->{img};
	my($x, $y, $xbot, $ybot, $xval);
	my @bottom;
#	
# Create a new polygon
	my $poly = GD::Polygon->new();
#
# Add the data points; data is organized as (x, ybot, ytop)
	for (my $i = 0; $i < @$ary; $i += 3)
	{
		next unless defined($$ary[$i]);
		$xval = $obj->{symDomain} ? ($i/3)+1 : $$ary[$i];
		($x, $y) = $obj->pt2pxl($xval, $$ary[$i+2]);
		($xbot, $ybot) = $obj->pt2pxl($xval, $$ary[$i+1]);
		$poly->addPt($x, $y);
		push @bottom, [$x, $ybot];
	}

	$poly->addPt($_->[0], $_->[1])
		foreach (reverse @bottom);

	# Draw a filled and a line polygon
	$img->filledPolygon($poly, $ci);
	$img->polygon($poly, $ci);

	1;
}

sub make_marker {
	my ($obj, $mtype, $mclr) = @_;

	my $brush = new GD::Image(9,9);
	my $white = $brush->colorAllocate(255, 255, 255);
	my $clr = $brush->colorAllocate(@{$colors{$mclr}});
	$brush->transparent($white);
	$mtype = $valid_shapes{$mtype};

# square, filled	
	$brush->filledRectangle(0,0,6,6,$clr),
	return $brush
		if ($mtype == 1);

# Square, open
	$brush->rectangle( 0, 0, 6, 6, $clr ),
	return $brush
		if ($mtype == 2);

# Cross, horizontal
	$brush->line( 0, 4, 8, 4, $clr ),
	$brush->line( 4, 0, 4, 8, $clr ),
	return $brush
		if ($mtype == 3);

# Cross, diagonal
	$brush->line( 0, 0, 8, 8, $clr ),
	$brush->line( 8, 0, 0, 8, $clr ),
	return $brush
		if ($mtype == 4);

# Diamond, filled
	$brush->line( 0, 4, 4, 8, $clr ),
	$brush->line( 4, 8, 8, 4, $clr ),
	$brush->line( 8, 4, 4, 0, $clr ),
	$brush->line( 4, 0, 0, 4, $clr ),
	$brush->fillToBorder( 4, 4, $clr, $clr ),
	return $brush
		if ($mtype == 5);

# Diamond, open
	$brush->line( 0, 4, 4, 8, $clr ),
	$brush->line( 4, 8, 8, 4, $clr ),
	$brush->line( 8, 4, 4, 0, $clr ),
	$brush->line( 4, 0, 0, 4, $clr ),
	return $brush
		if ($mtype == 6);

# Circle, filled
	$brush->arc( 4, 4, 8 , 8, 0, 360, $clr ),
	$brush->fillToBorder( 4, 4, $clr, $clr ),
	return $brush,
		if ($mtype == 7);

# must be Circle, open
	$brush->arc( 4, 4, 8, 8, 0, 360, $clr ),
	return $brush
		if ($mtype == 8);
#
#	dot - contributed by Andrea Spinelli
	$brush->setPixel( 4,4, $clr ),
	return  $brush
 		if ( $mtype == 10 );
}

sub getIcon {
	my ($obj, $icon, $isbar) = @_;
	my $pat = GD::Image->can('newFromGif') ? 
		'png|jpe?g|gif' : 'png|jpe?g';

	$obj->{errmsg} = 
	'Unrecognized icon file format. File qualifier must be .png, .jpg, ' . 
		(GD::Image->can('newFromGif') ? '.jpeg, or .gif.' : 'or .jpeg.'),
	return undef
		unless ($icon=~/\.($pat)$/i);

	$obj->{errmsg} = "Unable to open icon file $icon.",
	return undef
		unless open(ICON, "<$icon");

	my $iconimg = ($icon=~/\.png$/i) ? GD::Image->newFromPng(*ICON) :
	  ($icon=~/\.gif$/i) ? GD::Image->newFromGif(*ICON) :
	    GD::Image->newFromJpeg(*ICON);
	close(ICON);
	$obj->{errmsg} = "GD cannot read icon file $icon.",
	return undef
		unless $iconimg;

	my ($iconw, $iconh) = $iconimg->getBounds();
	$obj->{errmsg} = "Icon image $icon too wide for chart image.",
	return undef
		if (($isbar && ($iconw > $obj->{brushWidth})) ||
			($iconw > $obj->{plotWidth}));
		
	$obj->{errmsg} = "Icon image $icon too tall for chart image.",
	return undef
		if ($iconh > $obj->{plotHeight});
	return $iconimg;
}

sub drawIcons {
	my ($obj, $iconimg, $pxl, $pyb, $pxr, $pyt) = @_;
#
#	force the icon into the defined image area
#
	my ($iconw, $iconh) = $iconimg->getBounds();
	my $img = $obj->{img};
	if ($pxl == $pxr) {
		$pxl -= int($iconw/2);

		my $srcY = 0;
		my $h = $iconh;
#
#	handle candlestick points
		$img->copy($iconimg, $pxl, $pyb, 0, $srcY, $iconw, $h),
		return 1
			if ($pyt == 0);

		while ($pyb > $pyt) {	
			$h = $pyb - $pyt,
			$srcY = $iconh - $h,
				if ($iconh > ($pyb - $pyt));
			$pyb -= $h;
			$img->copy($iconimg, $pxl, $pyb, 0, $srcY, $iconw, $h);
		}
		return 1;
	}
#
#	must be histogram
	$pyb -= int($iconh/2); # this might need adjusting

	my $limX = $iconw;
	while ($pxl < $pxr) {	
		$limX = ($pxr - $pxl) if ($iconw > ($pxr - $pxl));
		$img->copy($iconimg, $pxl, $pyb, 0, 0, $limX, $iconh);
		$pxl += $limX;
	}
	1;
}

sub plot3DAxes {
	my ($obj) = @_;
	my $img = $obj->{img};
	my ($xl, $xh, $yl, $yh, $zl, $zh) = 
		($obj->{xl}, $obj->{xh}, $obj->{yl}, $obj->{yh}, $obj->{zl}, $obj->{zh});

	my $numRanges = scalar @{$obj->{data}};
	my $zbarw = ($obj->{zh} - $obj->{zl})/($obj->{zAxisLabel} ? $obj->{Zcard}*2 : 2);
	my $ishisto = ($obj->{plotTypes} & HISTO);

	$zl -= (0.8);
	$zh += $zbarw;
	my $yc = ($yl < 0) ? 0 : $yl;
	my @v = ($ishisto) ? 
	(
		$xl, $yl, $zl,	# bottom front left
		$xl, $yl, $zh,	# bottom rear left
		$xh, $yl, $zl,	# top front left
		$xh, $yl, $zh,	# top rear left
		$xl, $yh, $zl,	# bottom front right
		$xl, $yh, $zh,	# bottom rear right
		$xh, $yh, $zh,	# top rear right
#
#	in case floor is above bottom of graph
		$xl, $yl, $zl,	# bottom front left
		$xl, $yh, $zl,	# bottom front right
		$xl, $yh, $zh,	# bottom rear right
		$xl, $yl, $zh	# bottom rear left
	) :
#	its a barchart
	(
		$xl, $yl, $zl,	# bottom front left
		$xl, $yl, $zh,	# bottom rear left
		$xl, $yh, $zl,	# top front left
		$xl, $yh, $zh,	# top rear left
		$xh, $yl, $zl,	# bottom front right
		$xh, $yl, $zh,	# bottom rear right
		$xh, $yh, $zh,	# top rear right
#
#	in case floor is above bottom of graph
		$xl, $yc, $zl,	# bottom front left
		$xh, $yc, $zl,	# bottom front right
		$xh, $yc, $zh,	# bottom rear right
		$xl, $yc, $zh	# bottom rear left
	);
	my @xlatverts = ();
#
#	generate vertices of cabinet projection
#
	my ($i, $j);
	for ($i = 0; $i <= $#v; $i+=3) {
		push(@xlatverts, $obj->pt2pxl($v[$i], $v[$i+1], $v[$i+2]));
	}
#
#	draw left and rear wall, and floor
#
	for ($i = 0; $i <= $#axesverts; $i+=2) {
		$img->line($xlatverts[$axesverts[$i]],
			$xlatverts[$axesverts[$i]+1],
			$xlatverts[$axesverts[$i+1]],
			$xlatverts[$axesverts[$i+1]+1], $obj->{gridColor});
	}
#
#	draw grid lines if requested
#
	my ($gx, $gy, $hx, $hy);
	if ($obj->{horizGrid}) {
		my ($imax, $imin, $step) = 
			($obj->{yh}, $obj->{yl}, 
				($ishisto ? $obj->{horizStep} : $obj->{vertStep}));
		
		for ($i = $imin; $i < $imax; $i += $step) {
			($gx, $gy) = $obj->pt2pxl($xl, $i, $zl);
			($hx, $hy) = $obj->pt2pxl($xl, $i, $zh);
			$img->line($gx, $gy, $hx, $hy, $obj->{gridColor});
			($gx, $gy) = $obj->pt2pxl($xh, $i, $zh);
			$img->line($gx, $gy, $hx, $hy, $obj->{gridColor});
		}
	}
#
#	we forgot the axis labels!!!
#	draw Y-axis at rear-left-top corner
#	draw X-axis at front-right-bottom corner
#	draw Z-axis vertical from front-right-bottom along image edge
#
	my ($xal, $yal, $zal) = ($obj->{plotTypes} & HISTO) ? 
		($obj->{yAxisLabel}, $obj->{xAxisLabel}, $obj->{zAxisLabel}) :
		($obj->{xAxisLabel}, $obj->{yAxisLabel}, $obj->{zAxisLabel});
	if ($xal) {
		($gx