GD::Graph::radar - Make radial bar charts


GD-Graph-radar documentation Contained in the GD-Graph-radar distribution.

Index


Code Index:

NAME

Top

GD::Graph::radar - Make radial bar charts

SYNOPSIS

Top

  use GD::Graph::radar;

  my $radar = GD::Graph::radar->new(400, 400);

  my $image = $radar->plot([
      [qw( a    b  c    d    e    f    g  h    i )],
      [qw( 3.2  9  4.4  3.9  4.1  4.3  7  6.1  5 )]
  ]);

  print $image->png;  # Or ->gif, or ->jpeg, or...

DESCRIPTION

Top

This module is based on GD::Graph::pie with the exception of changes to the default settings, the draw_data method, and elimination of the pie specific code.

SEE ALSO

Top

GD::Graph

GD::Graph::pie

AUTHOR

Top

Original code by Brad J. Murray <bjm@phreeow.net>

Maintenance and CPAN distribution by Gene Boggs <gene@cpan.org>

GD::Graph by Martien Verbruggen <mgjv@tradingpost.com.au>

COPYRIGHT AND LICENSE

Top


GD-Graph-radar documentation Contained in the GD-Graph-radar distribution.

# $Id: radar.pm 800 2007-12-03 17:07:04Z gene $
package GD::Graph::radar;
$GD::Graph::radar::VERSION = '0.1002';
use base qw(GD::Graph);
use strict;
use warnings;
use Carp;
use GD;
use GD::Graph::colour qw(:colours :lists);
use GD::Graph::utils qw(:all);
use GD::Text::Align;

use constant PI => 4 * atan2(1, 1);
use constant ANGLE_OFFSET => 90;

my %Defaults = (
    # The angle at which to start the first data set 0 is pointing straight down
    start_angle => 0,

    # and some public attributes without defaults
    label => undef,

    # Absolute graphs always start at zero and cannot have negative
    # values.  non-absolute graphs start at minimum data set value
    absolute => 1,

    # number of scale markers to draw
    nmarkers => 6,

    # if set, draw a polygon connecting the apices of each line
    polygon => 1,

    # if defined, fill the polygon with the colour specified as
    # a hex rgb string.  Note that if one of the data
    # elements is zero, then the polygon will not fill as we fill
    # from the origin
    poly_fill => '#e4e4e4',
);

sub _has_default { 
    my $self = shift;
    my $attr = shift || return;
    exists $Defaults{$attr} || $self->SUPER::_has_default($attr);
}

sub initialise {
    my $self = shift;

    $self->SUPER::initialise();

    while (my ($key, $val) = each %Defaults) {
        $self->{$key} = $val;
    }

    $self->set_value_font(gdTinyFont);
    $self->set_label_font(gdSmallFont);
}

# PUBLIC methods
sub plot {
    my $self = shift;
    my $data = shift;

    $self->check_data($data) or return;
    $self->init_graph()      or return;
    $self->setup_text()      or return;
    $self->setup_coords()    or return;
    $self->draw_text()       or return;
    $self->draw_data()       or return;

    return $self->{graph};
}

sub set_label_font {  # (fontname)
    my $self = shift;

    $self->_set_font('gdta_label', @_) or return;

    $self->{gdta_label}->set_align('bottom', 'center');
}

sub set_value_font {  # (fontname)
    my $self = shift;

    $self->_set_font('gdta_value', @_) or return;

    $self->{gdta_value}->set_align('center', 'center');
}

# Inherit defaults() from GD::Graph
# Inherit checkdata from GD::Graph

# Setup the coordinate system and colours, calculate the
# relative axis coordinates in respect to the canvas size.

sub setup_coords() {
    my $self = shift;

    # Make sure we're not reserving space we don't need.
    my $tfh = $self->{title} ? $self->{gdta_title}->get('height') : 0;
    my $lfh = $self->{label} ? $self->{gdta_label}->get('height') : 0;

    # Calculate the bounding box for the graph, and
    # some width, height, and centre parameters
    $self->{bottom} = 
        $self->{height} - $self->{b_margin} -
        ( $lfh ? $lfh + $self->{text_space} : 0 );
    $self->{top} = 
        $self->{t_margin} + ( $tfh ? $tfh + $self->{text_space} : 0 );

    return $self->_set_error('Vertical size too small') 
        if $self->{bottom} - $self->{top} <= 0;

    $self->{left}  = $self->{l_margin};
    $self->{right} = $self->{width} - $self->{r_margin};

    return $self->_set_error('Horizontal size too small')
        if $self->{right} - $self->{left} <= 0;

    $self->{w} = $self->{right}  - $self->{left};
    $self->{h} = $self->{bottom} - $self->{top};

    $self->{xc} = ($self->{right}  + $self->{left}) / 2;
    $self->{yc} = ($self->{bottom} + $self->{top})  / 2;

    return $self;
}

# Inherit open_graph from GD::Graph

# Setup the parameters for the text elements
sub setup_text {
    my $self = shift;

    if ($self->{title}) {
        #print "'$s->{title}' at ($s->{xc},$s->{t_margin})\n";
        $self->{gdta_title}->set(colour => $self->{tci});
        $self->{gdta_title}->set_text($self->{title});
    }

    if ($self->{label}) {
        $self->{gdta_label}->set(colour => $self->{lci});
        $self->{gdta_label}->set_text($self->{label});
    }

    $self->{gdta_value}->set(colour => $self->{alci});

    return $self;
}

# Put the text on the canvas.
sub draw_text {
    my $self = shift;

    $self->{gdta_title}->draw($self->{xc}, $self->{t_margin}) 
        if $self->{title}; 
    $self->{gdta_label}->draw($self->{xc}, $self->{height} - $self->{b_margin})
        if $self->{label};
    
    return $self;
}

# Draw the data lines and the polygon
sub draw_data {
    my $self = shift;

    my $max_val = 0;
    my @values = $self->{_data}->y_values(1);   # for now, only one
    my $min_val = $values[0];
        my $scale = 1;

    for (@values) {    
        if ($_ > $max_val) { $max_val = $_; }
        if ($_ < $min_val) { $min_val = $_; }
    }

    $scale = $self->{absolute}
        ? ($self->{w} / 2) / $max_val
        : ($self->{w} / 2) / ($max_val - $min_val);
       
    my $ac = $self->{acci};  # Accent colour
    my $pb = $self->{start_angle};

    my $poly = new GD::Polygon;
    my @vertices = ();

    for (my $i = 0; $i < @values; $i++) {
        # Set the angles of each arm
        # Angle 0 faces down, positive angles are clockwise 
        # from there.
        #   ---
        #  /   \
        # |     |
        #  \ | /
        #   ---
        #    0
        # $pa/$pb include the start_angle (so if start_angle
        # is 90, there will be no pa/pb < 90.
        my $pa = $pb;
        $pb += my $slice_angle = 360 / @values;

        # Calculate the end points of the lines at the boundaries of
        # the pie slice
        my $radius = $values[$i] * $scale;

        $radius = 0 if $radius < 0 && $self->{absolute};

        my ($xe, $ye) = cartesian(
            $radius,
            $pa, 
            $self->{xc}, $self->{yc},
            $self->{h} / $self->{w}
        );

        $poly->addPt($xe, $ye) if $self->{polygon};

        push @vertices, [$xe, $ye];
    }

    # draw the apex polygon
    $self->{graph}->polygon($poly, $ac);

    if (defined $self->{poly_fill}) {
        my ($r, $g, $b) = GD::Graph::colour::hex2rgb($self->{poly_fill});

        my $fc = $self->{graph}->colorAllocate($r, $g, $b);

        $self->{graph}->fill($self->{xc}, $self->{yc}, $fc);
    }

    # draw markers
    my $mark_incr = 1;
    $mark_incr = $self->{absolute}
        ? int ($max_val / $self->{nmarkers})
        : int (($max_val - $min_val) / $self->{nmarkers});

    for (1 .. $self->{nmarkers}) {
        my $width = 2 * $_ * $mark_incr * $scale;

        $self->{graph}->arc(
            $self->{xc}, $self->{yc},
            $width,
            $width * ($self->{h} / $self->{w}), 
            0, 360,
            $ac,
        );
    }

    # draw radar value bars
    my $dc = $self->{graph}->colorAllocate(0, 0, 0);

    for (@vertices) {
        $self->{graph}->line(
            $self->{xc}, $self->{yc},
            $_->[0], $_->[1],
            $dc
        );
    }

    # draw labels
    $pb = $self->{start_angle};

    for (my $i = 0; $i < @values; $i++) {
        next unless $values[$i];

        my $pa = $pb;
        $pb += my $slice_angle = 360 / @values;

        next if $self->{suppress_angle} && $slice_angle <= $self->{suppress_angle};

        my ($xe, $ye) = cartesian(
              3 * $self->{w} / 8, $pa,
              $self->{xc}, $self->{yc},
              $self->{h} / $self->{w}
        );

        $self->put_slice_label($xe, $ye, $self->{_data}->get_x($i));
    }
       
    return $self;

}  #GD::Graph::radar::draw_data

# put the slice label on the pie
sub put_slice_label {
    my $self = shift;
    my ($x, $y, $label) = @_;

    return unless defined $label;

    $self->{gdta_value}->set_text($label);
    $self->{gdta_value}->draw($x, $y);
}

# return x, y coordinates from input
# radius, angle, center x and y and a scaling factor (height/width)
#
# $ANGLE_OFFSET is used to define where 0 is meant to be
sub cartesian {
    my ($r, $phi, $xi, $yi, $cr) = @_; 

    return (
        $xi + $r * cos (PI * ($phi + ANGLE_OFFSET) / 180),
        $yi + $cr * $r * sin (PI * ($phi + ANGLE_OFFSET) / 180)
    )
}

1;

__END__