################################################################################
#
#  kPerl Logics/Mathematics Library Group
#  Library for Data Plot Images
#
#  copyright (c)
#    Karol Szafranski, 2009
#    Institute of Molecular Biotechnology Jena, Dept. Genome Analysis, 1998-2004
#  author
#    Karol Szafranski, karol.szafranski@leibniz-fli.de
#
################################################################################
#
#  DESCRIPTION
#
# - purpose
#   Prepare image of a 2D data plot.
#
# - dependencies
#   This package relies on the GD package to create a bitmap image from
#   vector graphics commands.
#
# - compare to other packages
#   The intention of this package is much like well-known programs like
#   gnuplot or pgplot. Note that pgplot comes with a Perl interface (CPAN
#   PGPLOT), cf. http://www.aao.gov.au/local/www/kgb/pgperl/.
#
# - individual description of functions at the beginning of the code blocks
#
################################################################################
#
#  FUNCTIONS, DATA
#
#   @EXPORT
#   %_LibParam
#
# - basics
#   plot image ARGUMENT DATA STRUCTURE  see below
#
# - complete graph
#   &PlotStructUpdate
#   &Graph
#
# - plot
#   &DefaultPlot
#   &GraphPlotXY
#   &GraphPlotMarkarea
#
# - scale
#   &DefaultScale
#   &GraphScale
#
# - grid
#   &DefaultGrid
#   &DefaultGridLinStep
#   &DefaultGridLogStep
#   &GridStrMax
#   &GraphGrid
#   &LabelStrPos
#
# - map
#   &DefaultMap
#   &MapPosErect
#   &MapPosSpread
#   &GraphMap
#
#
#  STD OPTIONS
#
#   -debug      print debug protocol to STDERR
#
################################################################################
#
#  plot image ARGUMENT DATA STRUCTURE
#
# - data structure that has to be supplied to the module (entry via &Graph).
#
# explanation of symbols assigned to field labels:
# !  Fields of this kind must contain a proper value. Otherwise, the code
#    won't produce a valid image.
# -  Optional field. The code works out a default value if none is given by
#    the user.
# (  Field is filled only by internal computation. Any entered values will be
#    overwritten by the code.
#
# - colors may be specified either as:
#   - string referring to a color defined in the color library
#     (MainLib::Graphics.pm)
#   - reference to an array of RGB values.
#   - string syntax '#RRGGBB' in hexadecimal digits
#
# %graph             super-hash for everything
#   - width          X dimension of the final image
#                    If no values are given for width/height the calculation
#                    goes from plot to the complete graph, otherwise it goes
#                    vice versa.
#                    However, an accurate calculation from graph dimensioning
#                    to plot dimensioning fails cause it's quite complicated.
#                    The way to handle this would be first to create a
#                    fake graph just know how large the graph would get, and
#                    then to rescale everything to get a correctly scaled
#                    final graph that's being drawn. This is not implemented
#                    yet, sorry! So, currently, the values are ignored, but
#                    after graph creation they contain the resulting values
#                    for the graph.
#   - height         Y dimension of the final image
#   ( Diagon         Diagonal size of the plot area. Value is in pixels, but
#                    it may be floating point, however.
#   - BgColor        Color of the background (default: white).
#   - BgTranspar     Any boolean value will force the whole graph background
#                    to be transparent. For value 'plot' transparency takes
#                    effect only on the plot area.
#   ! plot           reference to array of %plot hashes
#   ( PosPlot        coordinate of the left upper corner of all plots, hash
#                    keys 'x','y',...
#   ! scale          reference to array of %scale hashes
#   ( img            resulting image (from GD module)
#
# %plot              hash for a data plot
#   - DimPixel       plot area dimensions in magnitudes of graphical pixels,
#                    data structure:
#                      { x=>$width, y=>$height }
#                    Defaults are either inherited from first plot data
#                    structure or worked out from primary defaults in %_LibParam.
#   - HeightRel      scaling of the plot area height relative to the width.
#                    That way a dimensioning of the plot area may be
#                    specified by a combination of {DimPixel}{x} and
#                    HeightRel.
#   - type           plot subtype, possible:
#                    PlotXY    display data pixels. This also includes lines
#                              between data pixels.
#                              detailed description of specifics below
#                    MarkArea  area marks placed on the plot area
#                              detailed description of specifics below
#   ! data           reference to data. See field 'DataType' for explanation
#                    on data formats.
#   - DataRangeRef   refer to data range from other plot (specify numeric ID).
#                    Only active specifications in $$pPlotRef{DataRange} will
#                    be mirrored from the referred plot sub-structure.
#   - DataRange      x/y data range boundaries, organized as:
#                      { x=>[$LeftX,$RightX], y=>[$LeftX,$RightX] }
#                    Values supplied here have priority over values resulting
#                    from 'DataRangeRef'. Defaults are derived from the
#                    minimum/maximum x/y coordinates found in the plot.
#   ( DataRangeEff   final effective x/y data range boundaries after processing
#                    by &Graph, organized as in 'DataRange'.
#   ! DataType       structure of plot data
#                    see '%plot - type PlotXY' or '%plot - type MarkArea' for
#                    details
#   ( FactorPlot     factors for calculation of the graphical coordinate from
#                    the data, hash with entries 'x','y',...
#   - ReprSize*      object size for the data representation either
#                    data-scaled or pixel-scaled. For the representation
#                    types column/pixel/line the size value refers to the
#                    thickness of the column/pixel/line.
#   - ReprSizeData   object size of the data representation in orders of the given
#                    data values. This field may serve for the calculation of
#                    value 'PixelScaleGraph' which will be used finally
#                    for scaling the data pixels.
#   - ReprSizePixel  object size of the data representation in orders of graphical
#                    pixels. This field overrides field ReprSizeData.
#   - ReprSD         add standard deviation display in data representation
#                    as a vertical bar on top and below the plot pixel
#   ( defaults       flag showing that defaults have been calculated
#   ( img            resulting image (from GD module) for the sub-structure.
#
# %plot - type PlotXY
#                    fields deviating from or expanding data structure
#                    described in '%plot'
#   ! DataType       structure of plot data with possible values (cf.
#                    definitions in database::Table.pm): A1y, AA, AC, AH, HCA,
#                    hash, MapPos.
#                    Default type is 'HCA', with column labels: 'x', 'y' (and
#                    optional 'SD'). Processing in &Graph forces the table data
#                    type to this format.
#                    Non-standard table data types are:
#                    A1y     one hash entry ('y') holding all values.
#                    hash    like 'HCA', but X values being strings
#                            *** implement me ***
#                            - The idea behind that is to create a
#                              column-type plot image.
#                            - A scale object needs to be constructed from
#                              the given plot data.
#                    MapPos  This data type is used to mark hot spots in the
#                            x data dimension. Those points are represented
#                            as vertical lines.
#                            The plot's ReprType is automatically set to
#                            'column'.
#   - ReprType       data representation type with possible values:
#                    circle, column, cross, crossdbl, crossgoth, line,
#                    pixel (default), plus, rect, star.
#   - ReprColor      color of data plot symbolization, default: black.
#
# %plot - type MarkArea
#                    fields deviating from / extending over data structure
#                    described in '%plot'
#   ! DataType       structure of plot data with possible values (cf.
#                    definitions in database::Table.pm): AA, AC, AH, HCA.
#                    Default type is 'HCA', with column labels: 'x1', 'x2',
#                    'y1', 'y2', and processing in &Graph forces the table data
#                    type to this format.
#   - ReprType       data representation type with possible values:
#                    rectangle (default).
#   - ReprColor      fill color for area symbolization, default: grey10.
#   - ReprColor2     boundary color for area symbolization, default: 'ReprColor'.
#
# %scale             hash for a plot scale
#   ! PlotNum        numeric ID of the according plot, default: 0.
#                    To use a numeric reference here instead of a data
#                    reference has the advantage that the data structure
#                    is representable in plain format and not only as a
#                    binary.
#   ( axis           axis of the plot data which is the scale is related to.
#                    This field is redundant in respect to field 'location'.
#                    However, the program needs this particular info.
#                    Possible values: x, y.
#   ! location       specify location and data reference of the scale, possible
#                    values: top, bottom, left, right, x, y
#                    (case-insensitive, but forced to lower case).
#                    There's no default for this field, cause the program
#                    has to know at least if to place the scale horizontally
#                    or vertically ('x' or 'y').
#   - hasLine        display basic scale line (boolean field), default: true.
#   - LineThick      thickness of basic scale line, default:
#                    $_LibParam{scale}{LineThick}
#   - color          color of the scale, default: color of the according
#                    plot. This field delegates as the default value
#                    for the color of all scale sub-structures.
#   - grid           reference to array of %grid hashes.
#   - map            reference to array of %map hashes.
#   ( DataRange      axis data range boundaries, organized as:
#                      [ $LeftX, $RightX ]
#   ( DimPixel       resulting scale area dimensions in magnitudes of graphical
#                    pixels, hash with entries:
#                    pre    overlap in relation to the plot area, at the left
#                           for horizontal location, or bottom side for
#                           vertical orientation of the scale
#                    post   overlap in relation to the plot area, at the right
#                           for horizontal location, or top side for vertical
#                           orientation of the scale
#                    ortho  extension orthogonally in relation to the axis
#                    x      width, including overlaps
#                    y      height, including overlaps
#   ( PosX           resulting X position in relation to the left upper corner
#                    of the plot area. Value is calculated by &Graph.
#   ( PosY           resulting Y position in relation to the left upper corner
#                    of the plot area. Value is calculated by &Graph.
#   ( img            resulting image (from GD module) for the sub-structure.
#
# %grid              hash for a grid, sub-structure of %scale
#   - calc           perform calculation for the grid values, possible
#                    statements: exp (= exp(10)), exp($val)
#   - offset         offset value of the grid
#                    This applies after an eventual calculation mode.
#   - StepVal        step value of the grid
#   - StepRel        specify a step value relative to the next higher level
#                    grid.
#   - LineLen        length of grid lines, default:
#                    - for the first grid: $_LibParam{grid}{LineLen}
#                    - for following grids: $_LibParam{grid}{LineLenNext}
#                      times the length of the next higher order grid
#   - LineThick      thickness of grid lines
#   - LineColor      color of the grid lines, default: color of the according
#                    scale.
#   - string         Label grid with value strings (boolean type field)
#   - StrOrient      orientation of the labelling strings in case of field
#                    'string' being set. Possible values: horizontal (default),
#                    vertical.
#   ( StrSense       orientation of the labelling strings relative to the
#                    direction of the data axis. Possible values: parallel,
#                    orthogonal.
#   - StrColor       color of the labelling strings, default: color of the
#                    grid lines.
#   ( DimPixel       resulting scale area dimensions in magnitudes of graphical
#                    pixels, hash with entries:
#                    StrMax   maximum string length if any
#                    pre      overlap in relation to the plot area, at the left
#                             for horizontal location, or bottom side for
#                             vertical orientation of the scale
#                    post     overlap in relation to the plot area, at the right
#                             for horizontal location, or top side for vertical
#                             orientation of the scale
#                    ortho    extension orthogonally in relation to the axis
#
# %map               hash for a labelling map, sub-structure of %scale.
#   - arrange        concept for how to arrange the labels over the scale
#                    without producing conflicts in form of label overlap.
#                    erect    try to arrange labels orthogonally. If an overlap
#                             conflict occurs, spread the overlapping labels
#                             to a minimum space value.
#                    spread   spread the labels homogeneously throughover the
#                             map.
#   ! data           reference to data, 'HCA' table data structure with column
#                    labels: 'pos', 'label'. Fields added by &Graph:
#                    PosEff  calculated pixel positions.
#   - LineColor      color of the map lines. As a default the lines have the
#                    color of the according scale.
#   - LineLen1       length of map lines in the orthogonal portion
#   - LineLen2       length of map lines in the occasionally traverse portion
#   - LineThick      thickness of map lines
#   ( StrOrient      orientation of the labelling strings. Possible values:
#                    horizontal (default), vertical.
#   ( StrSense       orientation of the labelling strings relative to the
#                    direction of the data axis. Possible values: parallel,
#                    orthogonal (only supported value).
#   - StrColor       color of the labelling strings, default: color of the
#                    map lines.
#   ( DimPixel       resulting scale area dimensions in magnitudes of graphical
#                    pixels, hash with entries:
#                    StrMax   maximum string length
#                    pre      overlap in relation to the plot area, at the left
#                             for horizontal location, or bottom side for
#                             vertical orientation of the scale
#                    post     overlap in relation to the plot area, at the right
#                             for horizontal location, or top side for vertical
#                             orientation of the scale
#                    ortho    extension orthogonally in relation to the axis
#
################################################################################
#
#  DEVELOPER'S NOTES
#
# - calling hierarchy:
#   - &Graph
#     - &DefaultPlot
#     - &GraphPlotXY
#     - &GraphPlotMarkarea
#     - &DefaultScale
#       - &DefaultGrid
#         - &DefaultGrid*Step
#         - &GridStrMax
#       - &DefaultMap
#         - &MapPosErect
#         - &MapPosSpread
#     - &GraphScale
#       - &GraphMap
#         - &LabelStrPos
#       - &GraphGrid
#         - &LabelStrPos
#
#
#  DEBUG, CHANGES, ADDITIONS
#
# - 20040830
#   replace orientation values "horizontal"/"vertical" and "parallel"/
#   "orthogonal" by numerical syntax.
#
# - 20040828
#   Attributes "DimPixel" and "HeightRel" are properties of the data display
#   area rather properties of a plot entry (the first entry).
#   - establish new graph substructure $$pGraph{area} with the mentioned
#     attributes. Each plot subentry defaults to $$pGraph{area}[0].
#   - move $$pGraph{plot}[0]{DimPixel} and $$pGraph{plot}[0]{HeightRel}
#     to $$pGraph{area}[0]
#
# - implement exponential gridding of log-scaled data
#   implementation started with passages in &DefaultGrid and new function
#   &DefaultGridLogStep
#
# - change $$pGraph{plot}[*]{FactorPlot} to $$pGraph{plot}[*]{DataTrafo}.
#   This will be useful in calculation of image coordinates from data
#   coordinates.
#
# - establish $$pGraph{plot}[*]{layer} in order to explicitly move a plot
#   element to a defined layer (front or back).
#
# - the left scale frequently gets cut by the left image border
#
# - look also for notes in the header of each function block
#
################################################################################

package Math::PlotImg;

# includes
use strict; #use warnings;  # mostly OK 20090124
use GD;  # this is not part of standard Perl distribution
  if ($GD::VERSION < 1.20 and ! $main::GlobStore{GdWarn}) {
    printf STDERR "WARNING: GD v%s doesn't support png\n", $GD::VERSION;
    $main::GlobStore{GdWarn} = 1;
  }
use MainLib::Data;
use MainLib::Misc qw(&MySub);
use MainLib::Graphics;
use Math::kCalc;
use Math::Round qw(&nearest &nearest_ceil);
use database::Table;

# symbol export
use Exporter qw(import);
our @EXPORT = qw (
  &PlotStructUpdate &Graph
  );

# package-wide constants and variables
my %_LibParam;


################################################################################
# complete graph
################################################################################


# update plot image data structure from older versions of this package
#
# INTERFACE
# - argument 1: reference to plot image data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
# DESCRIPTION
# - plot image data structure is prepared for re-run of &Graph.
#   Fields 'defaults' are deleted as far as possible.
#
sub PlotStructUpdate {
  my ($pGraph,%opt) = @_;
  my $debug = $opt{-debug};

  # work on gaph sub-structure
  delete $$pGraph{defaults};

  # work on plot sub-structures
  foreach my $pPlot (@{$$pGraph{plot}}) {
    if (ref($$pPlot{data}) eq 'HASH' and exists($$pPlot{data}{type})) {
      $$pPlot{DataType} = $$pPlot{data}{type};
      delete $$pPlot{data}{type};
    }
    if ($$pPlot{DataType} eq 'AC' and ref($$pPlot{data}) eq 'HASH') {
      $$pPlot{DataType} = 'HCA';
    }
    if (exists $$pPlot{DataRangeOff}) {
      defined ($$pPlot{DataRangeOff}{x}) and
        $$pPlot{DataRange}{x}[0] = $$pPlot{DataRangeOff}{x};
      defined ($$pPlot{DataRangeOff}{y}) and
        $$pPlot{DataRange}{y}[0] = $$pPlot{DataRangeOff}{y};
      delete $$pPlot{DataRangeOff};
    }
    if (exists $$pPlot{DataRangeEnd}) {
      defined ($$pPlot{DataRangeEnd}{x}) and
        $$pPlot{DataRange}{x}[1] = $$pPlot{DataRangeEnd}{x};
      defined ($$pPlot{DataRangeEnd}{y}) and
        $$pPlot{DataRange}{y}[1] = $$pPlot{DataRangeEnd}{y};
      delete $$pPlot{DataRangeEnd};
    }
    delete $$pPlot{defaults};
  }

  # work on scale sub-structures
  foreach my $pScale (@{$$pGraph{scale}}) {
    delete $$pScale{DataRangeOff};
    delete $$pScale{DataRangeEnd};
    delete $$pScale{defaults};
    foreach my $pGrid (map { @{$$pScale{$_}} } grep { exists $$pScale{$_} } 'grid', 'map') {
      delete $$pGrid{defaults};
    }
  }

  # debug
  if ($debug) {
    my $pGraphMin = &DataClone ($pGraph);
    foreach (@{$$pGraphMin{plot}}) { delete $_->{data} }
    printf STDERR "%s. updated plot image data structure:\n", &MySub;
    &DataPrint ($pGraphMin, -handle=>\*STDERR, -space=>2);
  }

  # exit SUB
  return 1;
}


# prepare image (currently, png only) from plot image data structure
#
# INTERFACE
# - argument 1: reference to plot image data structure
# - argument 2: path for image output
#
# - options:
#   -debug      [STD]
#   -save       save resulting graph image to file (argument is file path)
#
# - return val: success status (boolean)
#
sub Graph {
  my ($pGraph, %opt) = @_;
  my $debug = $opt{-debug};
  my $dbg2  = $debug ? $debug-1 : undef;
  my $PathOutput = $opt{-save};

  # calculating defaults for plot data structures
  $debug and printf STDERR "%s. finishing data structure plot %d from defaults\n", &MySub, 0;
  @{$$pGraph{plot}} or return undef;
  unless (&DefaultPlot ($pGraph, 0)) {
    printf STDERR "%s. ERROR: unable to finish plot data structure %d from defaults\n", &MySub, 0;
    return 0;
  }
  $$pGraph{width}  = $$pGraph{plot}[0]{DimPixel}{x};
  $$pGraph{height} = $$pGraph{plot}[0]{DimPixel}{y};
  $$pGraph{Diagon} = sqrt (($$pGraph{width} ** 2) + ($$pGraph{height} ** 2));
  $debug and printf STDERR "%s. primary graph dimensions: %d x %d\n", &MySub, $$pGraph{width}, $$pGraph{height};
  for (my $i=1; $i<@{$$pGraph{plot}}; ++$i) {
    unless (&DefaultPlot($pGraph,$i)) {
      die sprintf "%s. ERROR: unable to finish plot data structure %d from defaults\n", &MySub, $i;
    }
  }

  # create plot images
  for (my $i=0; $i<@{$$pGraph{plot}}; ++$i) {
    if ($$pGraph{plot}[$i]{type} eq 'PlotXY') {
      $$pGraph{plot}[$i]{img} = &GraphPlotXY ($pGraph, $i, -debug=>$dbg2);
    } elsif ($$pGraph{plot}[$i]{type} eq 'MarkArea') {
      $$pGraph{plot}[$i]{img} = &GraphPlotMarkarea ($pGraph, $i, -debug=>$dbg2);
    } else {
      die sprintf "%s. ERROR: unknown plot type %s\n", &MySub, $$pGraph{plot}[$i]{type};
    }
    unless ($$pGraph{plot}[$i]{img}) {
      die sprintf "%s. ERROR: unable to generate image for plot sub-structure %d\n", &MySub, $i;
    }
  }

  # finishing scale data structures from defaults
  for (my $i=0; $i<@{$$pGraph{scale}}; ++$i) {
    unless (&DefaultScale ($pGraph, $i, -debug=>$dbg2)) {
      $debug and printf STDERR "%s. ERROR signalled in \&DefaultScale\n", &MySub;
      return undef;
    }
  }

  # create scale images
  my (%ScaleDim,%ScaleOverlap);
  foreach (@{$$pGraph{scale}}) {
    unless ($_->{img} = &GraphScale ($pGraph, $_, -debug=>$dbg2)) {
      $debug and printf STDERR "%s. ERROR signalled in \&GraphScale\n", &MySub;
      return undef;
    }

    # calculate scale position, recalculate graph margins
    if ($_->{axis} eq 'x') {
      $ScaleOverlap{left}  = &Max ($ScaleOverlap{left}, $_->{DimPixel}{pre});
      $ScaleOverlap{right} = &Max ($ScaleOverlap{right}, $_->{DimPixel}{post});
      $_->{PosX} = - $_->{DimPixel}{pre};
      if ($_->{location} eq 'top') {
        $ScaleDim{top} += $_->{DimPixel}{y};
        $_->{PosY} = - $ScaleDim{top};
      } else {
        $ScaleDim{bottom} ||= 0;
        $_->{PosY} = $$pGraph{height} + $ScaleDim{bottom};
        $ScaleDim{bottom} += $_->{DimPixel}{y};
      }
    } else {
      $ScaleOverlap{bottom} = &Max ($ScaleOverlap{bottom}, $_->{DimPixel}{pre});
      $ScaleOverlap{top}    = &Max ($ScaleOverlap{top}, $_->{DimPixel}{post});
      $_->{PosY} = - $_->{DimPixel}{post};
      if ($_->{location} eq 'left') {
        $ScaleDim{left} += $_->{DimPixel}{x};
        $_->{PosX} = - $ScaleDim{left};
      } else {
        $ScaleDim{right} ||= 0;
        $_->{PosX} = $$pGraph{width}-1 + $ScaleDim{right};
        $ScaleDim{right} += $_->{DimPixel}{x};
      }
    }
  }

  # final graph dimensions
  foreach ('left', 'right', 'top', 'bottom') {
    $ScaleDim{$_} = &Max ($ScaleDim{$_}, $ScaleOverlap{$_});
  }
  $$pGraph{width}  += $ScaleDim{left} + $ScaleDim{right};
  $$pGraph{height} += $ScaleDim{top} + $ScaleDim{bottom};
  $$pGraph{PosPlot}{x} = $ScaleDim{left};
  $$pGraph{PosPlot}{y} = $ScaleDim{top};
  if ($debug) {
    printf STDERR "%s. final graph dimensions: %d x %d\n", &MySub, $$pGraph{width}, $$pGraph{height};
    printf STDERR "%s. graph left upper corner: (%d,%d)\n", &MySub, $$pGraph{PosPlot}{x}, $$pGraph{PosPlot}{y};
  }

  # basic styles
  $$pGraph{BgColor} ||= 'white';
  $$pGraph{BgColor} = &ColorFormat ($$pGraph{BgColor}) or return undef;

  # start creating the final image:
  # background fill, transparency
  my $img = new GD::Image ($$pGraph{width}, $$pGraph{height});
  { my $BgColor;
    # define background color
    if ($$pGraph{BgTranspar}) {
      $$pGraph{BgColor} ||= &ColorFormat('transparent');
      $BgColor = $img->colorAllocate (@{$$pGraph{BgColor}});
      $img->transparent ($BgColor);
    } else {
      $BgColor = $img->colorAllocate (@{$$pGraph{BgColor}});
    }
    # fill complete background
    $img->filledRectangle (0, 0, $$pGraph{width}, $$pGraph{height}, $BgColor);
    # transparency limited to plot area: re-allocate color, fill area
    if ($$pGraph{BgTranspar} and $$pGraph{BgTranspar} eq 'plot') {
      $BgColor = $img->colorAllocate (@{$$pGraph{BgColor}});
      $img->filledRectangle (
        $$pGraph{PosPlot}{x}, $$pGraph{PosPlot}{y},
        $$pGraph{PosPlot}{x}+$$pGraph{plot}[0]{DimPixel}{x}-1, $$pGraph{PosPlot}{y}+$$pGraph{plot}[0]{DimPixel}{y}-1,
        $BgColor);
    }
  }

  # insert plots
  for (my $i=$#{$$pGraph{plot}}; $i>=0; $i--) {
    $debug and printf STDERR "%s. inserting image for plot sub-structure %d\n", &MySub, $i;
    $img->copy ($$pGraph{plot}[$i]{img},
      $$pGraph{PosPlot}{x}, $$pGraph{PosPlot}{y},
      0, 0,
      $$pGraph{plot}[$i]{img}->getBounds(),
      );
  }

  # insert scales
  for (my $i=0; $i<@{$$pGraph{scale}}; ++$i) {
    $debug and printf STDERR "%s. inserting scale (ID %d) at position: (%d,%d)\n", &MySub,
      $i, $$pGraph{scale}[$i]{PosX}, $$pGraph{scale}[$i]{PosY};
    $img->copy ($$pGraph{scale}[$i]{img},
      $$pGraph{PosPlot}{x} + $$pGraph{scale}[$i]{PosX}, $$pGraph{PosPlot}{y} + $$pGraph{scale}[$i]{PosY},
      0, 0,
      $$pGraph{scale}[$i]{img}->getBounds(),
      );
  }

  # store and save image
  $$pGraph{img} = $img;
  if ($opt{-save}) {
    unless (open (OUTIMG, ">$PathOutput")) {
      $debug and printf STDERR "%s. ERROR: unable to open file %s for output\n", &MySub, $PathOutput||"''";
      return undef;
    }
    print OUTIMG $img->png();
    close OUTIMG;
  }

  # exit SUB
  return 1;
}


################################################################################
# plot
################################################################################

$_LibParam{plot} = {
  width     => 600,
  HeightRel => 0.8,
  };


# work out plot data structure values from defaults
#
# INTERFACE
# - argument 1: reference to plot data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
sub DefaultPlot {

  # function constants
  my %ColLabel = (
    PlotXY   => [ qw(x y SD) ],
    MarkArea => [ qw(x1 y1 x2 y2) ],
    MarkVal  => [ qw(x y) ],
    );
  my @BorderWinFunc = (\&Min,\&Max);

  # function parameters
  my ($pGraph,$PlotNum,%opt) = @_;
  my $debug = $opt{-debug};
  my $pPlot = $$pGraph{plot}[$PlotNum] or return undef;
  if ($$pPlot{defaults}) { return 1 }
  if (not $$pGraph{plot}[0]) { return 1 }
  my $pPlotRef = $PlotNum? $$pGraph{plot}[0] : undef;
  $debug and printf STDERR "%s. processing plot %d\n", &MySub, $PlotNum;

  ########################################################################
  # plot area

  # graphical area size from scratch
  unless ($PlotNum) {
    $$pPlot{DimPixel}{x} ||= $_LibParam{plot}{width};
    unless ($$pPlot{DimPixel}{y}) {
      # $$pPlot{HeightRel} may be zero!
      defined $$pPlot{HeightRel} or $$pPlot{HeightRel} = $_LibParam{plot}{HeightRel};
      $$pPlot{DimPixel}{y} = $$pPlot{DimPixel}{x} * $$pPlot{HeightRel};
    }
  }

  # graphical area size by reference
  else {
    $$pPlot{DimPixel}{x} = $$pPlotRef{DimPixel}{x};
    $$pPlot{DimPixel}{y} = $$pPlotRef{DimPixel}{y};
  }

  ########################################################################
  # plot type, data organization

  # plot type
  $$pPlot{type} ||= 'PlotXY';
  unless (exists $ColLabel{$$pPlot{type}}) { return undef }

  # force data type to 'HCA'
  if ($$pPlot{DataType} eq 'A1y') {
      # for table data structure type 'A1y'
      # storage in 'y' is heuristic!
    $$pPlot{data} = &TableConvert ('A1y', 'HCA', $$pPlot{data}{y}, -ColLabel=>$ColLabel{$$pPlot{type}});
    $$pPlot{DataType} = 'HCA';
  }
  if ($$pPlot{DataType} eq 'AA') {
    $$pPlot{data} = &TableConvert ('AA', 'HCA', $$pPlot{data}, -ColLabel=>$ColLabel{$$pPlot{type}});
    $$pPlot{DataType} = 'HCA';
  }
  if ($$pPlot{DataType} eq 'AC' and ref($$pPlot{data}) eq 'HASH') {  # former argument syntax
    $$pPlot{DataType} = 'HCA';
  }
  if ($$pPlot{DataType} eq 'AC') {
    $$pPlot{data} = &TableConvert ('AC', 'HCA', $$pPlot{data}, -ColLabel=>$ColLabel{$$pPlot{type}});
    $$pPlot{DataType} = 'HCA';
  }
  if ($$pPlot{DataType} eq 'AH') {
    $$pPlot{data} = &TableConvert ('AH', 'HCA', $$pPlot{data}, -ColLabel=>$ColLabel{$$pPlot{type}});
    $$pPlot{DataType} = 'HCA';
  }
  if ($$pPlot{DataType} eq 'MapPos') {
    $$pPlot{data}{y} = [ (1) x @{$$pPlot{data}{x}} ];
    $$pPlot{DataType} = 'HCA';
    $$pPlot{DataRange}{y} = [ 0, 1 ];
    $$pPlot{ReprType} = 'column';
  }
  unless ($$pPlot{DataType} eq 'HCA') {
    printf STDERR "%s. ERROR: unknown data type or unable to convert data type '%s' in plot %d\n", &MySub,
      $$pPlot{DataType}, $PlotNum;
    return undef;
  }

  # sort data coordinates
  if ($$pPlot{type} eq 'PlotXY') {
    ($$pPlot{data}{x},$$pPlot{data}{y}) = &Plot2dSort ($$pPlot{data}{x},$$pPlot{data}{y});
  }

  ########################################################################
  # data ranges, data->image transformation

  # data ranges
  # - values resulting from 'DataRangeRef'
  # - overwrite with defined values in 'DataRange'
  # - fill undefs with max/min data values
  if (defined $$pPlot{DataRangeRef}) {
    if ($$pPlot{DataRangeRef} == $PlotNum) { return 0 }
    &DefaultPlot ($pGraph, $$pPlot{DataRangeRef}, %opt);
    $pPlotRef = $$pGraph{plot}[$$pPlot{DataRangeRef}] or return 0;
  }
  foreach my $ItAxis ('x', 'y') {
    foreach my $ItBound (0, 1) {
      my @DataCol = grep{ exists($$pPlot{data}{$_}) and defined($$pPlot{data}{$_}) }
        grep{ m/$ItAxis/ } @{$ColLabel{$$pPlot{type}}};
      if (int(@DataCol)) {
        $$pPlot{DataRangeEff}{$ItAxis}[$ItBound] =
          &{$BorderWinFunc[$ItBound]} (map{ @{$$pPlot{data}{$_}} } @DataCol);
        if ($ItBound==1) {
          my $span=$$pPlot{DataRangeEff}{$ItAxis}[1]-$$pPlot{DataRangeEff}{$ItAxis}[0];
          $$pPlot{DataRangeEff}{$ItAxis}[0] -= $span/100;
          $$pPlot{DataRangeEff}{$ItAxis}[1] += $span/100;
        }
      } else {
        printf STDERR "%s. WARNING: undefined data column for axis %s, columns %s\n", &MySub,
          $ItAxis, join(' ',@{$ColLabel{$$pPlot{type}}});
      }
      if (defined($$pPlot{DataRangeRef}) and exists($$pPlotRef{DataRangeEff})) {
        $$pPlot{DataRangeEff}{$ItAxis}[$ItBound] = $$pPlotRef{DataRangeEff}{$ItAxis}[$ItBound];
      }
      if (exists($$pPlot{DataRange}{$ItAxis}) and defined($$pPlot{DataRange}{$ItAxis}[$ItBound])) {
        $$pPlot{DataRangeEff}{$ItAxis}[$ItBound] = $$pPlot{DataRange}{$ItAxis}[$ItBound];
      }
    }
  }

  # factors for dimensioning data to graphical plot
  unless ($$pPlot{DataRangeEff}{x}[1] - $$pPlot{DataRangeEff}{x}[0]) {
    $$pPlot{FactorPlot}{x} = 1;
  } else {
    $$pPlot{FactorPlot}{x} = ($$pPlot{DimPixel}{x} - 1) /
      ($$pPlot{DataRangeEff}{x}[1] - $$pPlot{DataRangeEff}{x}[0]);
  }
  unless ($$pPlot{DataRangeEff}{y}[1] - $$pPlot{DataRangeEff}{y}[0]) {
    $$pPlot{FactorPlot}{y} = 1;
  } else {
    $$pPlot{FactorPlot}{y} = ($$pPlot{DimPixel}{y} - 1) /
      ($$pPlot{DataRangeEff}{y}[1] - $$pPlot{DataRangeEff}{y}[0]);
  }

  ########################################################################
  # display attributes
  # - representation type, size
  # - representation color(s)

  if ($$pPlot{type} eq 'PlotXY') {
    $$pPlot{ReprType} ||= 'pixel';
    $$pPlot{ReprColor} ||= 'black';
    $$pPlot{ReprSizePixel} ||= 1;
  }

  if ($$pPlot{type} eq 'MarkArea') {
    $$pPlot{ReprType} ||= 'rectangle';
    $$pPlot{ReprColor} ||= 'grey10';
    $$pPlot{ReprColor2} ||= $$pPlot{ReprColor};
    $$pPlot{ReprSizePixel} ||= 1;
  }

  foreach my $itColor (grep{ exists($$pPlot{$_}) } 'ReprColor', 'ReprColor2') {
    unless ($$pPlot{$itColor} = &ColorFormat($$pPlot{$itColor})) {
      printf STDERR "%s. ERROR: unable to format color %s (%s), plot %d\n", &MySub,
        $itColor, $$pPlot{$itColor}, $PlotNum;
      return undef;
    }
  }

  ########################################################################
  # debug

  if ($debug) {
    printf STDERR "%s. plot dimension parameters:\n", &MySub;
    printf STDERR "  plot ID: %d\n", $PlotNum;
    printf STDERR "  plot type: %s\n", $$pPlot{type};
    foreach my $ItAxis ('x', 'y') {
      printf STDERR "  %s range arguments: %s..%s\n", $ItAxis, map { defined($_) ? $_:'undef' } @{$$pPlot{DataRange}{$ItAxis}};
      printf STDERR "  %s range effective: %s..%s\n", $ItAxis, @{$$pPlot{DataRangeEff}{$ItAxis}};
    }
    printf STDERR "  data->img recalc factor (x,y): %s..%s\n", @{$$pPlot{FactorPlot}}{'x','y'};
  }

  # return successfully
  $$pPlot{defaults} = 1;
  return 1;
}


# create image for plot sub-structure
#
# INTERFACE
# - argument 1: reference to plot data structure
#
# - options:
#   -debug      [STD]
#
# - return val: - reference to GD image object
#               - undef if an error occurred
#
# DESCRIPTION
# - default values are worked out in &DefaultPlot
# - plot data type has been forced to 'HCA' type (two arrays for each x and y
#   values, cmp. 'plot image ARGUMENT DATA STRUCTURE'.
#
sub GraphPlotXY {
  my ($pGraph,$PlotNum,%opt) = @_;
  my $debug = $opt{-debug};
  my $pPlot = $$pGraph{plot}[$PlotNum] or return undef;

  # missing data isn't really fatal, so we just report it
  if ((! @{$$pPlot{data}{x}} or ! @{$$pPlot{data}{y}}) and $debug) {
    printf STDERR "%s. WARNING: missing data values\n", &MySub;
  }

  # start creating the object's image
  my $img = new GD::Image ($$pPlot{DimPixel}{x}||1, $$pPlot{DimPixel}{y}||1);

  # allocate colors in plot image
  my %ImgPal;
  $ImgPal{background} = $img->colorAllocate (@{&ColorFormat('transparent',-format=>'ArrayInt')});
  $img->transparent ($ImgPal{background});
  $debug and printf STDERR "%s. allocating ReprColor (%s -> %s)\n", &MySub,
    $$pPlot{ReprColor}, join (' ', @{$$pPlot{ReprColor}});
  $ImgPal{PixelItem} = $img->colorAllocate (@{$$pPlot{ReprColor}});

  ##############################################################################
  # use brush?

  # create brush according to $$pPlot{ReprType}
  my $brush;  # needed throughout local lifetime of %img
  $debug and printf STDERR "%s. ReprSizePixel %d\n", &MySub, $$pPlot{ReprSizePixel};
  if ($$pPlot{ReprSizePixel} > 1) {
    $debug and printf STDERR "%s. creating brush for ReprType %s\n", &MySub, $$pPlot{ReprType};

    # brush size, color
    my $BrushRadius = &nearest_ceil (1, 0.5*$$pPlot{ReprSizePixel});
    $brush = new GD::Image ((2 * $BrushRadius) x 2);
    $ImgPal{BrushBg} = $brush->colorAllocate (@{&ColorFormat('transparent')});
    $brush->transparent ($ImgPal{BrushBg});
    $ImgPal{PixelBrush} = $brush->colorAllocate (@{$$pPlot{ReprColor}});

    # brush shape
    if ($$pPlot{ReprType} eq 'crossgoth') {
      $brush->setPixel (($BrushRadius) x 2, $ImgPal{PixelBrush});
    }
    else {  # includes pixel, line, circle, filled circle
      $brush->arc (($BrushRadius) x 4, 0, 360, $ImgPal{PixelBrush});
      if ($$pPlot{ReprType} eq 'pixel' or
          $$pPlot{ReprType} eq 'line' or
          $$pPlot{ReprType} =~ m/^filled/i) {
        $debug and printf STDERR "%s. perform filled representation\n", &MySub;
        $brush->fill (($BrushRadius) x 2, $ImgPal{PixelBrush});
      }
    }

    # activate brush
    $img->setBrush ($brush);
    $ImgPal{brush} = 0;
  }

  # no brush, just use pixel to draw
  else {
    $ImgPal{brush} = $ImgPal{PixelItem};
  }

  ##############################################################################
  # plot data

  # data coordinates have been sorted by x value in &DefaultPlot
  for (my $i=0; $i<@{$$pPlot{data}{y}}; ++$i) {
    my (%Coo1,%Coo2,%Coo3,%Coo4);

    # all: plot data pixel
    # eventually use brush
    $Coo1{x} = ($$pPlot{data}{x}[$i] - $$pPlot{DataRangeEff}{x}[0]) * $$pPlot{FactorPlot}{x};
    $Coo1{y} = ($$pPlot{DataRangeEff}{y}[1] - $$pPlot{data}{y}[$i]) * $$pPlot{FactorPlot}{y};
    $img->setPixel ($Coo1{x}, $Coo1{y}, $ImgPal{brush}||gdBrushed);

    # ReprType 'line': make line to next data pixel
    # eventually use brush
    if ($$pPlot{ReprType} eq 'line' and defined $$pPlot{data}{y}[$i+1]) {
      $Coo2{x} = ($$pPlot{data}{x}[$i+1] - $$pPlot{DataRangeEff}{x}[0]) * $$pPlot{FactorPlot}{x};
      $Coo2{y} = ($$pPlot{DataRangeEff}{y}[1] - $$pPlot{data}{y}[$i+1]) * $$pPlot{FactorPlot}{y};
      $img->line ($Coo1{x}, $Coo1{y}, $Coo2{x}, $Coo2{y}, $ImgPal{brush}||gdBrushed);
    }

    # ReprType 'column': make line from x axis to data pixel
    if ($$pPlot{ReprType} eq 'column') {
      $Coo2{y} = ($$pPlot{DataRangeEff}{y}[1] - $$pPlot{DataRangeEff}{y}[0]) * $$pPlot{FactorPlot}{y};
      $img->line ($Coo1{x}, $Coo1{y}, $Coo1{x}, $Coo2{y}, $ImgPal{brush}||gdBrushed);
    }

    # Represent standard deviation
    if ($$pPlot{ReprSD}) {
      $Coo3{x} = $Coo1{x} - &Max (2 * $$pPlot{ReprSizePixel}, 3);
      $Coo4{x} = $Coo1{x} + &Max (2 * $$pPlot{ReprSizePixel}, 3);
      $Coo2{y} = ($$pPlot{DataRangeEff}{y}[1] - ($$pPlot{data}{y}[$i] + $$pPlot{data}{SD}[$i])) * $$pPlot{FactorPlot}{y};
      $img->line ($Coo1{x}, $Coo1{y}, $Coo1{x}, $Coo2{y}, $ImgPal{PixelItem});
#      $img->line ($Coo3{x}, $Coo2{y}, $Coo4{x}, $Coo2{y}, $ImgPal{PixelItem});
      $Coo2{y} = ($$pPlot{DataRangeEff}{y}[1] - ($$pPlot{data}{y}[$i] - $$pPlot{data}{SD}[$i])) * $$pPlot{FactorPlot}{y};
      $img->line ($Coo1{x}, $Coo1{y}, $Coo1{x}, $Coo2{y}, $ImgPal{PixelItem});
#      $img->line ($Coo3{x}, $Coo2{y}, $Coo4{x}, $Coo2{y}, $ImgPal{PixelItem});
    }
  }

  # return plot image
  return $img;
}


# create image for plot sub-structure
#
# INTERFACE
# - argument 1: reference to plot data structure
#
# - options:
#   -debug      [STD]
#
# - return val: - reference to GD image object
#               - undef if an error occurred
#
# DESCRIPTION
# - default values are worked out in &DefaultPlot
# - plot data type has been forced to 'HCA' type (two arrays for each x and y
#   values, cmp. 'plot image ARGUMENT DATA STRUCTURE'.
#
sub GraphPlotMarkarea {
  my ($pGraph,$PlotNum,%opt) = @_;
  my $debug = $opt{-debug};
  my $pPlot = $$pGraph{plot}[$PlotNum] or return undef;

  # missing data isn't really fatal, so we just report it
  if ((! @{$$pPlot{data}{x1}} or ! @{$$pPlot{data}{y1}}) and $debug) {
    printf STDERR "%s. WARNING: missing data values\n", &MySub;
  }

  # start creating the object's image
  my $img = new GD::Image ($$pPlot{DimPixel}{x}, $$pPlot{DimPixel}{y});

  # allocate colors in plot image
  my %ImgPal;
  $ImgPal{background} = $img->colorAllocate (@{&ColorFormat('transparent')});
  $img->transparent ($ImgPal{background});
  $debug and printf STDERR "%s. allocating ReprColor (%s -> %s)\n", &MySub,
    $$pPlot{ReprColor}, join (' ', @{$$pPlot{ReprColor}});
  $ImgPal{PixelItemA} = $img->colorAllocate (@{$$pPlot{ReprColor}});
  $ImgPal{PixelItemB} = $img->colorAllocate (@{$$pPlot{ReprColor2}});

  ##############################################################################
  # plot data

  # data coordinates have been sorted by x value in &DefaultPlot
  for (my $i=0; $i<@{$$pPlot{data}{y1}}; ++$i) {
    my (%Coo1,%Coo2);

    # draw rectangle for area
    $Coo1{x} = ($$pPlot{data}{x1}[$i] - $$pPlot{DataRangeEff}{x}[0]) * $$pPlot{FactorPlot}{x};
    $Coo1{y} = ($$pPlot{DataRangeEff}{y}[1] - $$pPlot{data}{y1}[$i]) * $$pPlot{FactorPlot}{y};
    $Coo2{x} = ($$pPlot{data}{x2}[$i] - $$pPlot{DataRangeEff}{x}[0]) * $$pPlot{FactorPlot}{x};
    $Coo2{y} = ($$pPlot{DataRangeEff}{y}[1] - $$pPlot{data}{y2}[$i]) * $$pPlot{FactorPlot}{y};
    $img->rectangle ($Coo1{x}, $Coo1{y}, $Coo2{x}, $Coo2{y}, $ImgPal{PixelItemB});

    # fill rectangle
    # method $img->filledRectangle does not work, however
    $img->fill (0.5*($Coo1{x}+$Coo2{x}), 0.5*($Coo1{y}+$Coo2{y}), $ImgPal{PixelItemA});
  }

  # return plot image
  return $img;
}


################################################################################
# scale
################################################################################

$_LibParam{scale} = {
  LineThick => 1,
  StrSpace  => 3,
  font      => gdMediumBoldFont,
  };
$_LibParam{scale}{FontHeight} = $_LibParam{scale}{font}->height;
$_LibParam{scale}{FontWidth}  = $_LibParam{scale}{font}->width;


# work out scale data structure values from defaults
#
# INTERFACE
# - argument 1: reference to graph data structure
# - argument 2: ID number of the scale data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
# DESCRIPTION
# - The whole graph data structure is needed to allow a decision
#   where to place the scale in case of 'location' = 'x'/'y'.
# - The code needs the according plot data structure to be precompiled.
#
sub DefaultScale {
  my ($pGraph, $ScaleNum, %opt) = @_;
  my $debug = $opt{-debug};
  my $pScale = $$pGraph{scale}[$ScaleNum]        or return undef;
  my $pPlot  = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  if ($debug) {
    printf STDERR "%s. scale data finishing, ID $ScaleNum\n", &MySub;
    printf STDERR "  plot ID: %d\n", $$pScale{PlotNum};
  }

  # reference to plot axis
  $$pScale{location} = lc $$pScale{location};
  if      ($$pScale{location} =~ m/^(top|bottom|x)$/) {
    $$pScale{axis} = 'x';
  } elsif ($$pScale{location} =~ m/^(left|right|y)$/) {
    $$pScale{axis} = 'y';
  } else { return undef }

  # location and data range
  my %CtFlank;
  if ($$pScale{axis} eq 'x') {
    if (!$$pScale{location} or $$pScale{location}=~m/^x$/) {
      for (my $i=0; $i<$ScaleNum; ++$i) {
        if ($$pGraph{scale}[$i]{location} eq 'top')    { ++$CtFlank{top} }
        if ($$pGraph{scale}[$i]{location} eq 'bottom') { ++$CtFlank{bottom} }
      }
      $$pScale{location} = ($CtFlank{bottom}<=$CtFlank{top}) ? 'bottom' : 'top';
    }
  } else {
    if (!$$pScale{location} or $$pScale{location}=~m/^y$/) {
      for (my $i=0; $i<$ScaleNum; ++$i) {
        if ($$pGraph{scale}[$i]{location} eq 'left')  { ++$CtFlank{left} }
        if ($$pGraph{scale}[$i]{location} eq 'right') { ++$CtFlank{right} }
      }
      $$pScale{location} = ($CtFlank{left}<=$CtFlank{right}) ? 'left' : 'right';
    }
  }
  $$pScale{DataRange} = [ @{$$pPlot{DataRangeEff}{$$pScale{axis}}} ];

  # scale overall style
  if (!$$pScale{grid} and !$$pScale{map}) {
    $$pScale{grid} = [ {string=>1}, {} ];
  }

  # debug
  if ($debug) {
    printf STDERR "  scale sub-objects: grids=%d, maps=%d\n", int @{$$pScale{grid}}, int @{$$pScale{map}};
    printf STDERR "  plot data axis: %s\n", $$pScale{axis};
    printf STDERR "  scale location: %s\n", $$pScale{location};
  }

  # styles
  unless (defined $$pScale{hasLine}) {
    $$pScale{hasLine} = $$pScale{grid} ? 1 : 0;
  }
  $$pScale{LineThick} ||= $_LibParam{scale}{LineThick};
  $$pScale{color} ||= $$pPlot{ReprColor};
  $$pScale{color} = &ColorFormat ($$pScale{color}) or return undef;

  # expand grid parameters, calculate dimensions
  my $pDim = $$pScale{DimPixel} = {};
  for (my $CtGrid=0; $CtGrid<int(@{$$pScale{grid}||[]}); ++$CtGrid) {
    unless (&DefaultGrid ($pGraph, $ScaleNum, $CtGrid, %opt)) {
      $debug and printf STDERR "%s. ERROR: signalled in \&DefaultGrid\n", &MySub;
      splice @{$$pScale{grid}}, $CtGrid, 1;
      $$pScale{grid}[$CtGrid] ? redo : last;
    }

    # update dimension maxima
    my $pGrid = $$pScale{grid}[$CtGrid];
    foreach ('pre', 'post', 'ortho') {
      $$pDim{$_} = &Max ($$pDim{$_}, $$pGrid{DimPixel}{$_});
    }
  }

  # expand map parameters, calculate dimensions
  for (my $CtMap=0; $CtMap<int(@{$$pScale{map}||[]}); ++$CtMap) {
    unless (&DefaultMap ($pGraph,$ScaleNum,$CtMap,%opt)) {
      $debug and printf STDERR "%s. ERROR signalled in DefaultMap()\n", &MySub;
      splice @{$$pScale{map}}, $CtMap, 1;
      $$pScale{map}[$CtMap] ? redo : last;
    }

    # update dimension maxima
    my $pMap = $$pScale{map}[$CtMap];
    foreach ('pre', 'post', 'ortho') {
      $$pDim{$_} = &Max ($$pDim{$_}, $$pMap{DimPixel}{$_});
    }
  }

  # final dimensions
  my ($DirSense,$DirAsense) = ($$pScale{axis} eq 'x') ? ('x','y') : ('y','x');
  $$pDim{$DirSense} = $$pPlot{DimPixel}{$DirSense} + $$pDim{pre} + $$pDim{post};
  $$pDim{$DirAsense} = $$pDim{ortho};

  # final debug
  if ($debug) {
    printf STDERR "%s. finishing scale data, ID $ScaleNum (continued)\n", &MySub;
    printf STDERR "  orthogonal dim.: %d\n", $$pDim{ortho};
    printf STDERR "  overlap: pre=%d, post=%d\n", $$pDim{pre}, $$pDim{post};
    printf STDERR "  scale area: x=%d, y=%d\n", $$pDim{x}, $$pDim{y};
  }

  # return successfully
  return 1;
}


# create sub-image for scale
#
# INTERFACE
# - argument 1: reference to graph data structure
# - argument 2: reference to scale data structure
#
# - return val: - reference to GD image object
#               - undef if an error occurred
#
sub GraphScale {
  my ($pGraph,$pScale,%opt) = @_;
  my $debug = $opt{-debug};
  my $pPlot = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  $debug and printf STDERR "%s. drawing scale data\n", &MySub;

  # start creating the object's image
  my $img = new GD::Image ($$pScale{DimPixel}{x}, $$pScale{DimPixel}{y});

  # allocate colors
  my %ImgPal;
  $ImgPal{background} = $img->colorAllocate (@{&ColorFormat('transparent')});
  $img->transparent ($ImgPal{background});
  $ImgPal{scale} = $img->colorAllocate (@{$$pScale{color}});

  # draw basic scale line
  my (%Coo1,%Coo2);
  if ($$pScale{hasLine}) {
    if ($$pScale{axis} eq 'x') {
      $Coo1{x} = $$pScale{DimPixel}{pre};
      $Coo2{x} = $$pScale{DimPixel}{x} - 1 - $$pScale{DimPixel}{post};
      if ($$pScale{location} eq 'top') {
        $Coo1{y} = $Coo2{y} = $$pScale{DimPixel}{y} - 1 - int(0.48*$$pScale{LineThick});
      } else {
        $Coo1{y} = $Coo2{y} = 0.48 * $$pScale{LineThick};
      }
    } else {
      $Coo1{y} = $$pScale{DimPixel}{pre};
      $Coo2{y} = $$pScale{DimPixel}{y} - 1 - $$pScale{DimPixel}{post};
      if ($$pScale{location} eq 'left') {
        $Coo1{x} = $Coo2{x} = $$pScale{DimPixel}{x} - 1 - int(0.48*$$pScale{LineThick});
      } else {
        $Coo1{x} = $Coo2{x} = 0.48 * $$pScale{LineThick};
      }
    }
    $img->line ($Coo1{x}, $Coo1{y}, $Coo2{x}, $Coo2{y}, $ImgPal{scale});
  }

  # maps
  for (my $i=$#{$$pScale{map}||[]}; $i>=0; --$i) {
    &GraphMap ($pGraph, $pScale, $i, $img, %opt);
  }

  # grids
  for (my $i=0; $i<@{$$pScale{grid}||[]}; ++$i) {
    &GraphGrid ($pGraph, $pScale, $i, $img, %opt);
  }

  # return image for scale
  return $img;
}


################################################################################
# grid
################################################################################

$_LibParam{grid} = {
  IntervNum0  => 15,
  IntervNum1  => 10,
  IntervBonus => 1.8,
  StepNum     => [2,5,10],
  LineLen     => 6,
  LineLenNext => 0.5,
  };


# work out full grid parameters from defaults
#
# INTERFACE
# - argument 1: reference to graph data structure
#               this is essentially needed in &DefaultGrid*Step
# - argument 2: ID number of the scale data structure
# - argument 3: ID number of the grid data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
sub DefaultGrid {
  my ($pGraph,$ScaleNum,$GridNum,%opt) = @_;
  my $debug = $opt{-debug};
  my $pScale = $$pGraph{scale}[$ScaleNum] or return undef;
  my $pGrid  = $$pScale{grid}[$GridNum]   or return undef;

  # string orientation if any
  if ($$pGrid{string}) {
    $$pGrid{StrOrient} = lc ($$pGrid{StrOrient}||'horizontal');
    $$pGrid{StrOrient} =~ m/^(horizontal|vertical)$/ or return undef;
    my %DicSense = (
      horizontal => { x=>'parallel',   y=>'orthogonal' },
      vertical   => { x=>'orthogonal', y=>'parallel' },
      );
    $$pGrid{StrSense} = $DicSense{$$pGrid{StrOrient}}{$$pScale{axis}};
  }

#  # exponential gridding measure  *** not implemented ***
#  # (use it for log-scaled data)
#  if ($$pGrid{calc} =~ m/^\s*exp(\s*\((.+)\))?/) {
#    &DefaultGridLogStep ($pGraph, $ScaleNum, $GridNum, $2||10, %opt)
#  }
#  
#  # linear gridding measure
#  else {
    # grid step size and offset
    $$pGrid{StepVal}
      or &DefaultGridLinStep ($pGraph, $ScaleNum, $GridNum, %opt)
      or return undef;
    unless ($$pGrid{offset}) {
      $$pGrid{offset} = $$pScale{DataRange}[0]
        - &ModuloExt($$pScale{DataRange}[0],$$pGrid{StepVal});
      if ($$pGrid{offset} < $$pScale{DataRange}[0]) {
        $$pGrid{offset} += $$pGrid{StepVal};
      }
    }
#  }

  # meanwhile debug
  if ($debug) {
    printf STDERR "%s. finishing grid data, scale %d(%s), grid %d\n", &MySub,
      $ScaleNum, $$pScale{location}, $GridNum;
    printf STDERR "  data range borders: offset=%s, end=%s\n",
      $$pScale{DataRange}[0], $$pScale{DataRange}[1];
    printf STDERR "  grid offset: %s\n", $$pGrid{offset};
    printf STDERR "  grid strings: %s\n", $$pGrid{string} ? 'yes':'no';
  }

  # grid styles
  $$pGrid{LineThick} ||= $$pScale{LineThick};
  $$pGrid{LineLen}   ||= $GridNum ?
    $$pScale{grid}[$GridNum-1]{LineLen} * $_LibParam{grid}{LineLenNext}
    : $_LibParam{grid}{LineLen};
  $$pGrid{LineColor} ||= $$pScale{color};
  $$pGrid{LineColor} = &ColorFormat ($$pGrid{LineColor}) or return undef;
  if ($$pGrid{string}) {
    $$pGrid{StrColor} ||= $$pGrid{LineColor};
    $$pGrid{StrColor} = &ColorFormat ($$pGrid{StrColor}) or return undef;
  }

  # dimensions
  # - $olap: maximum overlap of image elements (grid labels) in parallel direction
  #   beyond the ends of the basic scale line
  #   $$pDim{pre} = $$pDim{post} = $olap;
  # - $$pDim{ortho}: pixel dimension in orthogonal direction
  my $pDim = $$pGrid{DimPixel} = {};
  my $olap = $$pGrid{LineThick};
  $$pDim{ortho} = $$pGrid{LineLen} + $$pGrid{LineThick};
  if ($$pGrid{string}) {
    $$pDim{ortho} += $_LibParam{scale}{StrSpace};
    $$pDim{StrMax} = &GridStrMax ($pGraph, $ScaleNum, $GridNum, %opt);
    if ($$pGrid{StrSense} eq 'parallel') {
      $olap += 0.5 * $$pDim{StrMax} * $_LibParam{scale}{FontWidth};
      $$pDim{ortho} += $_LibParam{scale}{FontHeight};
    } else {
      $olap += 0.5 * $_LibParam{scale}{FontHeight};
      $$pDim{ortho} += $$pDim{StrMax} * $_LibParam{scale}{FontWidth};
    }
  }
  $$pDim{pre} = $$pDim{post} = $olap;
  # debug, continued
  if ($debug) {
    printf STDERR "  line length: %d\n", $$pGrid{LineLen};
    printf STDERR "  line thickness: %d\n", $$pGrid{LineThick};
    printf STDERR "  line color: (%s)\n", join (',', @{$$pGrid{LineColor}});
    if ($$pGrid{string}) {
    printf STDERR "  spacing of labels: %d\n", $_LibParam{scale}{StrSpace};
    printf STDERR "  string orientation: %s, %s\n",
      $$pGrid{StrSense}||"''", $$pGrid{StrOrient}||"''";
    printf STDERR "  character dimensions: %d x %d\n",
      $_LibParam{scale}{FontWidth}, $_LibParam{scale}{FontHeight};
    printf STDERR "  max. string length: %d\n", $$pGrid{DimPixel}{StrMax};
    printf STDERR "  string color: (%s)\n", join (',', @{$$pGrid{StrColor}});
    }
    printf STDERR "  orthogonal dimens.: %d\n", $$pGrid{DimPixel}{ortho};
    printf STDERR "  overlap: pre=%d, post=%d\n",
      $$pGrid{DimPixel}{pre}, $$pGrid{DimPixel}{post};
  }

  # return successfully
  return 1;
}


# work out grid step size from defaults
#
# INTERFACE
# - argument 1: reference to graph data structure
#               this is essentially needed for retrieval of $pPlot!
# - argument 2: ID number of the scale data structure
# - argument 3: ID number of the grid data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
sub DefaultGridLinStep {
  my ($pGraph, $ScaleNum, $GridNum, %opt) = @_;
  my $debug = $opt{-debug};
  my $pScale = $$pGraph{scale}[$ScaleNum]        or return undef;
  my $pPlot  = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  my $pGrid  = $$pScale{grid}[$GridNum]          or return undef;

  # size of scale's value interval
  my $StepSize;  # minimum interval size
  my ($GridRange, $NumInterv, $FactorInterv, $bStepSizeBonus);
  if ($GridNum == 0) {
    $GridRange = $$pScale{DataRange}[1] - $$pScale{DataRange}[0];
    $NumInterv = $_LibParam{grid}{IntervNum0};
    $FactorInterv = ($$pScale{axis} eq 'x') ? $$pPlot{DimPixel}{x} : $$pPlot{DimPixel}{y};
    $StepSize = $GridRange / ($NumInterv * ($FactorInterv / $$pGraph{Diagon}));
  }
  # dimension differently if grid is 2nd or lower order
  else {
    if ($$pGrid{StepRel}) {
      $StepSize = $$pScale{grid}[$GridNum-1]{StepVal} * $$pGrid{StepRel};
    } else {
      $GridRange = $$pScale{grid}[$GridNum-1]{StepVal};
      $NumInterv = $_LibParam{grid}{IntervNum1};
      $StepSize = 0.999 * $GridRange / $NumInterv;
    }
  }

  # step size bonus (by factor) in case of:
  # - there're no string labels for this grid
  # - string labels are orthogonally oriented in relation to the scale axis
  if (!$$pGrid{string} or $$pGrid{StrSense} eq 'orthogonal') {
    $bStepSizeBonus = 1;  # flag for debug report
    $StepSize /= $_LibParam{grid}{IntervBonus};
  }

  # meanwhile debug
  if ($debug) {
    printf STDERR "%s. grid step size calculation, grid ID $GridNum:\n", &MySub;
    print  STDERR "  grid total range: $GridRange\n";
    print  STDERR "  min. step value: $StepSize\n";
    printf STDERR "  step size bonus: %s\n", $bStepSizeBonus ? 'yes':'no';
  }

  # decimal exponent and first digit of step size value
  my ($StepDigit,$StepExp) = (sprintf('%E',$StepSize) =~ m/^(\d+(?:\.\d+)?)E?([+-]?\d+)?$/);
  if ($StepDigit % 1) { ++ $StepDigit }

  # determine final step size value
  foreach (@{$_LibParam{grid}{StepNum}}) {
    if ($_ >= $StepDigit) {
      $$pGrid{StepVal} = ($_.'.0E'.$StepExp) + 0;
      $debug and print  STDERR "  step size: $$pGrid{StepVal}\n";
      return 1;
    }
  }

  # return with error
  printf STDERR "%s. code ERROR in looking for step size digit\n", &MySub;
  return undef;
}


# work out grid step size from defaults
#
# INTERFACE
# - argument 1: reference to graph data structure
#               this is needed essentially for retrieval of $pPlot!
# - argument 2: ID number of the scale data structure
# - argument 3: ID number of the grid data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
sub DefaultGridLogStep {
  my ($pGraph, $ScaleNum, $GridNum, $ExpVal, %opt) = @_;
  my $debug = $opt{-debug};
  my $pScale = $$pGraph{scale}[$ScaleNum]        or return undef;
  my $pPlot  = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  my $pGrid  = $$pScale{grid}[$GridNum]          or return undef;

  # size of scale's value interval
  my $StepSize;  # minimum interval size
  my ($GridRange, $NumInterv, $FactorInterv, $bStepSizeBonus);
  if ($GridNum == 0) {
    $GridRange = $$pScale{DataRange}[1] - $$pScale{DataRange}[0];
    $NumInterv = $_LibParam{grid}{IntervNum0};
    $FactorInterv = ($$pScale{axis} eq 'x') ? $$pPlot{DimPixel}{x} : $$pPlot{DimPixel}{y};
    $StepSize = $GridRange / ($NumInterv * ($FactorInterv / $$pGraph{Diagon}));
  }
  # dimension differently if grid is 2nd or lower order
  else {
    $GridRange = $$pScale{grid}[$GridNum-1]{StepVal};
    $NumInterv = $_LibParam{grid}{IntervNum1};
    $StepSize = $GridRange / $NumInterv;
  }

  # step size bonus (by factor) in case of:
  # - there're no string labels for this grid
  # - string labels are orthogonally oriented in relation to the scale axis
  if (! $$pGrid{string} or $$pGrid{StrSense} eq 'orthogonal') {
    $bStepSizeBonus = 1;  # flag for debug report
    $StepSize /= $_LibParam{grid}{IntervBonus};
  }

  # meanwhile debug
  if ($debug) {
    printf STDERR "%s. grid step size calculation, grid ID $GridNum:\n", &MySub;
    print  STDERR "  grid total range: $GridRange\n";
    print  STDERR "  min. step value: $StepSize\n";
    printf STDERR "  step size bonus: %s\n", $bStepSizeBonus ? 'yes':'no';
  }

  # decimal exponent and first digit of step size value
  my ($StepDigit,$StepExp) = (sprintf('%E',$StepSize) =~ m/^(\d+(?:\.\d+)?)E?([+-]?\d+)?$/);
  if ($StepDigit % 1) { $StepDigit ++; }

  # determine final step size value
  foreach (@{$_LibParam{grid}{StepNum}}) {
    if ($_ >= $StepDigit) {
      $$pGrid{StepVal} = ($_.'.0E'.$StepExp) + 0;
      $debug and print  STDERR "  step size: $$pGrid{StepVal}\n";
      return 1;
    }
  }

  # return with error
  printf STDERR "%s. code ERROR in looking for step size digit\n", &MySub;
  return undef;
}


# calculate grid string character size
#
# INTERFACE
# - argument 1: reference to graph data structure
# - argument 2: ID number of the scale data structure
# - argument 3: ID number of the grid data structure
# - return val: - string size (count of letters)
#               - undef if an error occurred
#
sub GridStrMax {
  my ($pGraph,$ScaleNum,$GridNum,%opt) = @_;
  my $debug = $opt{-debug};
  my $pScale = $$pGraph{scale}[$ScaleNum] or return undef;
  my $pGrid  = $$pScale{grid}[$GridNum]   or return undef;

  # loop over grid positions
  my (@string,@StrLen);
  for (my $i=$$pGrid{offset}; $i<=$$pScale{DataRange}[1]; $i+=$$pGrid{StepVal}) {
    my $StrCurr = sprintf ('%s', &nearest($$pGrid{StepVal},$i));
    push @StrLen, length($StrCurr);
    push @string, $StrCurr;
  }
  $debug and printf STDERR "  %s. array of grid strings: %s\n", &MySub, join(',',@string);

  # return
  return &Max(@StrLen);
}


# print grid and grid strings into image
#
# INTERFACE
# - argument 1: reference to graph data structure
#               this is needed essentially for retrieval of $pPlot!
# - argument 2: reference to scale data structure
# - argument 3: ID number of the grid data structure
# - argument 4: reference to the scale sub-image
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
sub GraphGrid {
  my ($pGraph,$pScale,$GridNum,$img,%opt) = @_;
  my $pPlot = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  my $pGrid = $$pScale{grid}[$GridNum]          or return undef;

  # allocate colors
  my %ImgPal;
  $ImgPal{line} = $img->colorAllocate (@{$$pGrid{LineColor}});
  if ($$pGrid{string}) {
    $ImgPal{string} = $img->colorAllocate (@{$$pGrid{StrColor}});
  }

  # Coo1: axis end, Coo2: peak end
  my (%Coo1,%Coo2,%CooM);
  # calculate constant coordinate values
  if      ($$pScale{location} eq 'left') {
    $Coo1{x} = $$pScale{DimPixel}{x} - $$pScale{LineThick};
    $Coo2{x} = $Coo1{x} - $$pGrid{LineLen};
  } elsif ($$pScale{location} eq 'right') {
    $Coo1{x} = $$pScale{LineThick};
    $Coo2{x} = $Coo1{x} + $$pGrid{LineLen};
  } elsif ($$pScale{location} eq 'top') {
    $Coo1{y} = $$pScale{DimPixel}{y} - $$pScale{LineThick};
    $Coo2{y} = $Coo1{y} - $$pGrid{LineLen};
  } elsif ($$pScale{location} eq 'bottom') {
    $Coo1{y} = $$pScale{LineThick};
    $Coo2{y} = $Coo1{y} + $$pGrid{LineLen};
  }

  # loop over grid positions
  for (my $i=$$pGrid{offset};
       $i>=$$pScale{DataRange}[0] and
       $i<=$$pScale{DataRange}[1];
       $i+=$$pGrid{StepVal}) {

    # flexible coordinate
    if ($$pScale{axis} eq 'x') {
      $Coo1{x} = $$pScale{DimPixel}{pre} +
        ($i - $$pScale{DataRange}[0]) * $$pPlot{FactorPlot}{x};
      $Coo2{x} = $Coo1{x};
    } else {
      $Coo1{y} = $$pScale{DimPixel}{post} +
        ($$pScale{DataRange}[1] - $i) * $$pPlot{FactorPlot}{y};
      $Coo2{y} = $Coo1{y};
    }

    # draw tic line
    $img->line ($Coo1{x}, $Coo1{y}, $Coo2{x}, $Coo2{y}, $ImgPal{line});

    # print string
    if ($$pGrid{string}) {
      my $string = sprintf ('%s', &nearest($$pGrid{StepVal},$i));
      ($CooM{x}, $CooM{y}) = &LabelStrPos ($string, $pScale, $pGrid, %opt);
      ($$pGrid{StrOrient} eq 'horizontal') ?
        $img->string   ($_LibParam{scale}{font}, $Coo2{x}+$CooM{x}, $Coo2{y}+$CooM{y},
	        $string, $ImgPal{string}) :
        $img->stringUp ($_LibParam{scale}{font}, $Coo2{x}+$CooM{x}, $Coo2{y}+$CooM{y},
	        $string, $ImgPal{string}) ;
    }
  }

  # return successfully
  return 1;
}


# work out relative string position for a label
#
# INTERFACE
# - argument 1: label string
# - argument 2: reference to scale data structure
# - argument 3: reference to label object data structure (\%grid or \%map)
#
# - options:
#   -debug      [STD]
#
# - return val: movement vector (x, y)
#
sub LabelStrPos {
  my ($string,$pScale,$pLabelObj,%opt) = @_;
  my $orient = $$pLabelObj{StrOrient};
  my $loc    = $$pScale{location};
  my $space  = $_LibParam{scale}{StrSpace};
  my $high   = $_LibParam{scale}{FontHeight};
  my $width  = $_LibParam{scale}{FontWidth};

  # movement coordinate calculation matrix
  # index hierarchy:
  # - graphical dimension: x, y
  # - string orientation: horizontal, vertical
  # - location of the grid: left, right, top, bottom
  # - formula consisting of factor*$strlen, constant pixel
  my %FactMove = (
     x => {
       horizontal => {
         left   => [ -1  *$width,  0         - $space+1 ],
         right  => [  0         ,  0         + $space+1 ],
         top    => [ -0.5*$width,  0         +      0 ],
         bottom => [ -0.5*$width,  0         +      0 ],
         },
       vertical => {
         left   => [  0         , -1  *$high - $space ],
         right  => [  0         ,  0         + $space ],
         top    => [  0         , -0.5*$high +      0 ],
         bottom => [ -1  *$width, -0.5*$high +      0 ],
         },
       },
     y => {
       horizontal => {
         left   => [  0         , -0.5*$high +      0 ],
         right  => [  0         , -0.5*$high +      0 ],
         top    => [  0         , -1  *$high - $space ],
         bottom => [  0         ,  0         + $space ],
         },
       vertical => {
         left   => [ +0.5*$width,  0         +      0 ],
         right  => [ +0.5*$width,  0         +      0 ],
         top    => [  0         ,  0         - $space ],
         bottom => [ +1  *$width,  0         + $space ],
         },
       },
     );

  # return movement vector
  return (
    length($string) * $FactMove{x}{$orient}{$loc}[0]
      + $FactMove{x}{$orient}{$loc}[1],
    length($string) * $FactMove{y}{$orient}{$loc}[0]
      + $FactMove{y}{$orient}{$loc}[1],
    );
}


################################################################################
# map
################################################################################

$_LibParam{map} = {
  LabelSpace => 1,
  LineLen1   => 8,
  LineLen2   => 15,
  };


# work out full scale map parameters from defaults
#
# INTERFACE
# - argument 1: reference to graph data structure
# - argument 2: ID number of the scale data structure
# - argument 3: ID number of the map data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
sub DefaultMap {
  my ($pGraph,$ScaleNum,$MapNum,%opt) = @_;
  my $debug = $opt{-debug};
  my $pScale = $$pGraph{scale}[$ScaleNum]        or return undef;
  my $pPlot  = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  my $pMap   = $$pScale{map}[$MapNum]            or return undef;
  my $pDim   = $$pMap{DimPixel} = {};

  # string orientation
  $$pMap{StrSense} = lc $$pMap{StrSense};
  $$pMap{StrSense} = 'orthogonal';
  my %DicSense = (
    parallel   => { x=>'horizontal', y=>'vertical' },
    orthogonal => { x=>'vertical',   y=>'horizontal' },
    );
  $$pMap{StrOrient} = $DicSense{$$pMap{StrSense}}{$$pScale{axis}};

  # sort label coordinates
  ($$pMap{data}{pos}, $$pMap{data}{label})
    = (&Plot2dSort ($$pMap{data}{pos}, $$pMap{data}{label}));

  # label positions
  $$pMap{arrange} ||= 'erect';
  if ($$pMap{arrange} eq 'erect') {
    &MapPosErect ($pGraph, $pScale, $MapNum, %opt) or return undef;
  } elsif ($$pMap{arrange} eq 'spread') {
    &MapPosSpread ($pGraph, $pScale, $MapNum, %opt) or return undef;
  } else { return undef }

  # debug
  if ($debug) {
    printf STDERR "%s. map data finishing, map ID $MapNum\n", &MySub;
    printf STDERR "  string orientation: %s, %s\n", $$pMap{StrSense}||"''", $$pMap{StrOrient}||"''";
    printf STDERR "  data range: %s..%s\n", @{$$pScale{DataRange}};
    print  STDERR "  label coordinates: ";
    for (my $i=0; $i<@{$$pMap{data}{pos}}; ++$i) {
    printf STDERR "(%d,%s) ", $$pMap{data}{pos}[$i], $$pMap{data}{label}[$i];
    }
    print  STDERR "\n";
  }

  # map styles
  $$pMap{LineThick} ||= $$pScale{LineThick};
  $$pMap{LineLen1}  ||= $MapNum ?
    $$pScale{DimPixel}{ortho} : $_LibParam{map}{LineLen1};
  $$pMap{LineLen2}  ||= $_LibParam{map}{LineLen2};
  $$pMap{LineColor} ||= $$pScale{color};
  $$pMap{LineColor} = &ColorFormat ($$pMap{LineColor}) or return undef;
  $$pMap{StrColor}  ||= $$pMap{LineColor};
  $$pMap{StrColor} = &ColorFormat ($$pMap{StrColor}) or return undef;

  # dimensions
  if (! $$pDim{StrMax}) {
    foreach (@{$$pMap{data}{label}}) {
      $$pDim{StrMax} = &Max ($$pDim{StrMax}, length $_);
    }
  }
  my $StrLenA = length $$pMap{data}{label}[0];
  my $StrLenO = length $$pMap{data}{label}[$#{$$pMap{data}{PosEff}}];
  my $PosA = $$pMap{data}{PosEff}[0];
  my $PosO = $$pMap{data}{PosEff}[$#{$$pMap{data}{PosEff}}];
  $$pDim{ortho}  = $$pMap{LineLen1} + $$pMap{LineLen2};
  $$pDim{ortho} += $_LibParam{scale}{StrSpace};
  $$pDim{pre} = $$pDim{post} = $$pMap{LineThick};
  if ($$pMap{StrSense} eq 'parallel') {
    $$pDim{pre}   += &Max (-$PosA + 0.5*$StrLenA*$_LibParam{scale}{FontWidth}, 0);
    $$pDim{post}  += &Max ( $PosO - $$pPlot{DimPixel}{$$pScale{axis}} + 0.5*$StrLenO*$_LibParam{scale}{FontWidth}, 0);
    $$pDim{ortho} += $_LibParam{scale}{FontHeight};
  } else {
    $$pDim{pre}   += &Max (-$PosA + 0.5*$_LibParam{scale}{FontHeight}, 0);
    $$pDim{post}  += &Max ( $PosO - $$pPlot{DimPixel}{$$pScale{axis}} + 0.5*$_LibParam{scale}{FontHeight}, 0);
    $$pDim{ortho} += $$pDim{StrMax} * $_LibParam{scale}{FontWidth};
  }
  if ($debug) {
    printf STDERR "  string color: (%s)\n", join (',', @{$$pMap{StrColor}});
    printf STDERR "  line length: level1=%d, level2=%d\n", $$pMap{LineLen1}, $$pMap{LineLen2};
    printf STDERR "  line thickness: %d\n", $$pMap{LineThick};
    printf STDERR "  line color: (%s)\n", join (',', @{$$pMap{LineColor}});
    printf STDERR "  orthogonal dimens.: %d\n", $$pMap{DimPixel}{ortho};
    printf STDERR "  overlap: pre=%d, post=%d\n", $$pMap{DimPixel}{pre}, $$pMap{DimPixel}{post};
  }

  # return successfully
  return 1;
}


# calculate label positions
#
# INTERFACE
# - argument 1: reference to graph data structure
#               this is needed essentially for retrieval of $pPlot!
# - argument 2: reference to scale data structure
# - argument 3: ID number of the map data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
# DESCRIPTION
# - label positions @{$$pMap{data}{PosEff}} are calculated as pixel
#   positions relative to the scale offset (which may be right off the scale
#   area border).
#
sub MapPosErect {
  my ($pGraph, $pScale, $MapNum, %opt) = @_;
  my $pPlot   = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  my $pMap    = $$pScale{map}[$MapNum]            or return undef;
  my $pPosEff = $$pMap{data}{PosEff} = [];
  my $factor = $$pPlot{FactorPlot}{$$pScale{axis}};
  my $DistMax = $_LibParam{scale}{FontHeight} + $_LibParam{map}{LabelSpace};

  # loop over sorted label positions
  my $PosPrev = -$DistMax;
  foreach (@{$$pMap{data}{pos}}) {
    my $pos = ($_ - $$pScale{DataRange}[0]) * $factor;
    my $DistMiss = $PosPrev + $DistMax - $pos;

    # handle label conflict
    if ($DistMiss > 0) {
      my $CtPrev = $#$pPosEff;
      my $MoveL = &Min (0.5 * $DistMiss, 0.5 * $DistMax);
      $$pPosEff[$CtPrev] -= $MoveL;
      $pos += $DistMiss - $MoveL;
      $CtPrev --;
      while ($CtPrev >= 0 and ($DistMiss = $$pPosEff[$CtPrev] + $DistMax - $$pPosEff[$CtPrev+1]) > 0) {
        $$pPosEff[$CtPrev] -= $DistMiss;
        $CtPrev --;
      }
    }

    # enter position value
    push @$pPosEff, $pos;
    $PosPrev = $pos;
  }

  # return successfully
  return 1;
}


# calculate label positions
#
# INTERFACE
# - argument 1: reference to graph data structure
#               this is needed essentially for retrieval of $pPlot!
# - argument 2: reference to scale data structure
# - argument 3: ID number of the map data structure
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
# DESCRIPTION
# - label positions @{$$pMap{data}{PosEff}} are calculated as pixel
#   positions relative to the scale offset (which may be right off the scale
#   area border).
#
sub MapPosSpread {
  my ($pGraph, $pScale, $MapNum, %opt) = @_;
  my $pPlot = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  my $pMap  = $$pScale{map}[$MapNum]            or return undef;

  # value range and step size
  my $Range = $$pPlot{DimPixel}{$$pScale{axis}};
  my ($StepPos,$StepSize);
  if (@{$$pMap{data}{pos}} == 1) {
    $StepPos = 0.5 * $Range;
  } else {
    $StepSize = $Range / (@{$$pMap{data}{pos}} - 1);
  }

  # loop over label positions
  $$pMap{data}{PosEff} = [];
  foreach (@{$$pMap{data}{pos}}) {
    push @{$$pMap{data}{PosEff}}, $StepPos;
    $StepPos += $StepSize;
  }

  # return
  return 1;
}


# work out map step size from defaults
#
# INTERFACE
# - argument 1: reference to graph data structure
#               this is needed essentially for retrieval of $pPlot!
# - argument 2: reference to scale data structure
# - argument 3: ID number of the map data structure
# - argument 4: reference to the scale sub-image
#
# - options:
#   -debug      [STD]
#
# - return val: success status (boolean)
#
# DESCRIPTION
# - label positions @{$$pMap{data}{PosEff}} are pre-calculated as pixel
#   positions relative to the scale offset (which may be right off the scale
#   area border).
#
sub GraphMap {
  my ($pGraph, $pScale, $MapNum, $img, %opt) = @_;
  my $pPlot = $$pGraph{plot}[$$pScale{PlotNum}] or return undef;
  my $pMap  = $$pScale{map}[$MapNum]            or return undef;

  # allocate colors
  my %ImgPal;
  $ImgPal{line} = $img->colorAllocate (@{$$pMap{LineColor}});
  $ImgPal{string} = $img->colorAllocate (@{$$pMap{StrColor}});

  # line coordinates: Coo1 = axis end, Coo2 = peak end
  my (%Coo1, %Coo2, %Coo3, %CooM);
  # calculate constant coordinate values
  if (0) {
  } elsif ($$pScale{location} eq 'left') {
    $Coo1{x} = $$pScale{DimPixel}{x} - $$pScale{LineThick};
    $Coo2{x} = $Coo1{x} - $$pMap{LineLen1};
    $Coo3{x} = $Coo2{x} - $$pMap{LineLen2};
  } elsif ($$pScale{location} eq 'right') {
    $Coo1{x} = $$pScale{LineThick};
    $Coo2{x} = $Coo1{x} + $$pMap{LineLen1};
    $Coo3{x} = $Coo2{x} + $$pMap{LineLen2};
  } elsif ($$pScale{location} eq 'top') {
    $Coo1{y} = $$pScale{DimPixel}{y} - $$pScale{LineThick};
    $Coo2{y} = $Coo1{y} - $$pMap{LineLen1};
    $Coo3{y} = $Coo2{y} - $$pMap{LineLen2};
  } elsif ($$pScale{location} eq 'bottom') {
    $Coo1{y} = $$pScale{LineThick};
    $Coo2{y} = $Coo1{y} + $$pMap{LineLen1};
    $Coo3{y} = $Coo2{y} + $$pMap{LineLen2};
  }

  # loop over map positions
  for (my $i=0; $i<@{$$pMap{data}{pos}}; ++$i) {

    # flexible coordinate
    if ($$pScale{axis} eq 'x') {
      $Coo2{x} = $Coo1{x} = $$pScale{DimPixel}{pre} +
        ($$pMap{data}{pos}[$i] - $$pScale{DataRange}[0]) * $$pPlot{FactorPlot}{x};
      $Coo3{x} = $$pScale{DimPixel}{pre} + $$pMap{data}{PosEff}[$i];
    } else {
      $Coo2{y} = $Coo1{y} = $$pScale{DimPixel}{post} +
        ($$pScale{DataRange}[1] - $$pMap{data}{pos}[$i]) * $$pPlot{FactorPlot}{y};
      $Coo3{y} = $$pScale{DimPixel}{y} - $$pScale{DimPixel}{pre} -
        $$pMap{data}{PosEff}[$i];
    }

    # draw lines
    $img->line ($Coo1{x}, $Coo1{y}, $Coo2{x}, $Coo2{y}, $ImgPal{line});
    $img->line ($Coo2{x}, $Coo2{y}, $Coo3{x}, $Coo3{y}, $ImgPal{line});

    # print string
    my $string = $$pMap{data}{label}[$i];
    ($CooM{x},$CooM{y}) = &LabelStrPos ($string, $pScale, $pMap, %opt);
    ($$pMap{StrOrient} eq 'horizontal') ?
      $img->string   ($_LibParam{scale}{font}, $Coo3{x}+$CooM{x}, $Coo3{y}+$CooM{y}, $string, $ImgPal{string}) :
      $img->stringUp ($_LibParam{scale}{font}, $Coo3{x}+$CooM{x}, $Coo3{y}+$CooM{y}, $string, $ImgPal{string}) ;
  }

  # return
  return 1;
}


1;
# $Id: PlotImg.pm,v 1.20 2018/06/05 18:02:56 szafrans Exp $
