#!/usr/bin/perl
#
# DW::Graphs - creates graphs for the statistics system
#
# Authors:
#      Anarres <anarres@dreamwidth.org>
#
# Copyright (c) 2010-2011 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.

package DW::Graphs;

use strict;
use warnings;
require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
use GD::Graph;
use GD::Graph::bars;
use GD::Graph::hbars;
use GD::Graph::pie;
use GD::Graph::lines;
use GD::Text;
use GD::Graph::colour;
use YAML ();

=head1 NAME

DW::Graphs - creates graphs for the statistics system

=head1 SYNOPSIS

  use DW::Graphs;
  my $pie = DW::Graphs::pie( { Ponies => 5, Rainbows => 1, Unicorns => 3 } );
  # Display $pie->png using your favorite library
  my $bar = DW::Graphs::bar( [ 5, 1, 3 ], [ qw( Ponies Rainbows Unicorns ) ],
                                  'Critter', 'Count' );
  # Display $bar->png using your favorite library
  my $bars = DW::Graphs::bar2( [ [ qw( Ponies Rainbows Unicorns ) ],
                                 [ 5, 1, 3 ], [ 2, 0, 1 ] ],
                               'Critter', 'Count',
                               [ qw( Plain Sparkly ) ] );
  # Display $bars->png using your favorite library
  my $lines = DW::Graphs::lines( [ [ qw( Ponies Rainbows Unicorns ) ],
                                   [ 5, 1, 3 ], [ 2, 0, 1 ] ],
                                 'Critter', 'Count',
                                 [ qw( Plain Sparkly ) ] );
  # Display $lines->png using your favorite library

=cut

# Define colours - the arrays can be over-ridden by config file
my $background   = '#f7f7f7';
my $nearly_white = '#f8f8f8';
my $textclr      = '#c1272d';
my $clrs         = [ '#7eafd2', '#f3973e', '#77cba2', '#edd344', '#a5c640', '#d87ba9' ];
my $dark_clrs    = [ '#11061b', '#920d00', '#0d3d1b', '#490045', '#4e1b05' ];

# Default font and font sizes
my %fonts = (
    font           => "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf",
    title_size     => 14,
    value_size     => 10,
    label_size_pie => 10,
    label_size     => 12,
    axis_size      => 10,
    legend_size    => 12,
);

=head1 API

=head2 C<< DW::Graphs::pie( $data [, $config_filename ] ) >>

Creates pie chart from $data (slice_label => slice_value hashref), using
optional $config_filename to override defaults. Returns a GD::Graph::pie.

See ~/dw/bin/dev/graphs_usage.pl for more detailed usage information.

=cut

sub pie {
    my ( $pie_ref, $config_filename ) = @_;

    # Sort the key-value pairs of %$pie_ref by value:
    # @pie_labels is the keys and @pie_values is the values
    my @pie_labels = sort { $pie_ref->{$a} cmp $pie_ref->{$b} } keys %$pie_ref;
    my @pie_values = map  { $pie_ref->{$_} } @pie_labels;

    # Package the data in a way that makes GD::Graph happy
    my $pie = [ [@pie_labels], [@pie_values] ];

    # Default settings (can be over-ridden by config file)
    my %settings = (
        transparent    => 0,               # Set this to 1 for transparent background
        accentclr      => $nearly_white,
        start_angle    => 90,              # Angle of first slice of pie, 0 = 6 o'clock
        suppress_angle => 5,               # Smaller slices than this have no labels
        bgclr          => $background,
        dclrs          => $clrs,
        labelclr       => '#000000',
        valuesclr      => '#000000',
        textclr        => $textclr,
        '3d'           => 0,
    );
    my $image_width  = 300;                # Image width in pixels - can be over-ridden
    my $image_height = 300;

    # If there is a config file, get any settings from it
    if ( defined $config_filename ) {
        my $config = YAML::LoadFile("$LJ::HOME/etc/$config_filename");

        # Image size
        $image_width = $config->{image_width}
            if defined $config->{image_width};
        $image_height = $config->{image_height}
            if defined $config->{image_height};

        # Over-ride %settings with settings in config file, if they exist
        foreach my $k ( keys %settings ) {
            $settings{$k} = $config->{$k}
                if defined $config->{$k};
        }

        # Over-ride %fonts with font settings in config file, if they exist
        my $config_fonts = $config->{fonts};
        if ( defined $config_fonts ) {
            $fonts{$_} = $config_fonts->{$_} foreach keys %$config_fonts;
        }
    }

    # Create graph object
    my $graph = GD::Graph::pie->new( $image_width, $image_height );
    $graph->set(%settings) or die $graph->error;

    # Fonts defined at top in %fonts, and can be over-ridden by config file
    $graph->set_title_font( $fonts{font}, $fonts{title_size} );
    $graph->set_value_font( $fonts{font}, $fonts{value_size} );
    $graph->set_label_font( $fonts{font}, $fonts{label_size_pie} );

    my $gd = $graph->plot($pie) or die $graph->error;
    return $gd;
}

=head2 C<< DW::Graphs::bar( $values_ref, $labels_ref, $xlabel, $ylabel [, $config_filename ] ) >>

Creates bar chart from $values_ref (value arrayref), $labels_ref (label
arrayref), $xlabel, $ylabel, using optional $config_filename to override
defaults. Returns a GD::Graph::bars.

See ~/dw/bin/dev/graphs_usage.pl for more detailed usage information.

=cut

sub bar {
    my ( $values_ref, $labels_ref, $xlabel, $ylabel, $config_filename ) = @_;

    # Package the input as required by GD Graph
    my $input_ref = [ $labels_ref, $values_ref ];

    # Default settings (can be over-ridden by config file)
    my %settings = (
        x_label      => "\r\n$xlabel",
        y_label      => $ylabel,
        show_values  => 1,
        values_space => 1,               # Pixels between top of bar and value above
        b_margin     => 20,              # Bottom margin (makes space for labels)
        t_margin     => 50,              # Top margin - makes space for value above highest bar
        y_min_value  => 0.0,             # Stop scale going below zero

        bgclr      => $background,
        fgclr      => 'white',
        boxclr     => '#f4eedc',         # Shaded-in background
        long_ticks => 1,                 # Background grid lines
        accentclr  => $background,       # Colour of grid lines

        labelclr     => '#000000',
        axislabelclr => '#000000',
        legendclr    => '#000000',
        valuesclr    => '#000000',
        textclr      => $textclr,
        transparent  => 0,               # 1 for transparent background
        dclrs        => $clrs,
    );
    my $image_width  = 500;              # Image width in pixels - can be over-ridden
    my $image_height = 350;

    # If there is a config file, get any settings from it
    if ( defined $config_filename ) {
        my $config = YAML::LoadFile("$LJ::HOME/etc/$config_filename");

        # Image size
        $image_width = $config->{image_width}
            if defined $config->{image_width};
        $image_height = $config->{image_height}
            if defined $config->{image_height};

        # Over-ride %settings with settings in config file, if they exist
        foreach my $k ( keys %settings ) {
            $settings{$k} = $config->{$k}
                if defined $config->{$k};
        }

        # Over-ride %fonts with font settings in config file, if they exist
        my $config_fonts = $config->{fonts};
        if ( defined $config_fonts ) {
            $fonts{$_} = $config_fonts->{$_} foreach keys %$config_fonts;
        }
    }

    # Create graph object
    my $graph = GD::Graph::bars->new( $image_width, $image_height );
    $graph->set(%settings) or die $graph->error;

    # Fonts defined at top in %fonts, and can be over-ridden by config file
    $graph->set_title_font( $fonts{font}, $fonts{title_size} );
    $graph->set_x_label_font( $fonts{font}, $fonts{label_size} );
    $graph->set_y_label_font( $fonts{font}, $fonts{label_size} );
    $graph->set_x_axis_font( $fonts{font}, $fonts{axis_size} );
    $graph->set_y_axis_font( $fonts{font}, $fonts{axis_size} );
    $graph->set_values_font( $fonts{font}, $fonts{value_size} );

    # Make the graph
    my $gd = $graph->plot($input_ref) or die $graph->error;
    return $gd;
}

=head2 C<< DW::Graphs::bar2( $values_ref, $labels_ref, $xlabel, $ylabel [, $config_filename ] ) >>

Creates bar chart with two or more sets of data from $ref ([ [ @value_labels ],
[ @dataset1 ], [ @dataset2 ], ... ]), $xlabel, $ylabel, $names_ref (label
arrayref, must have 1 element per dataset), using optional $config_filename to
override defaults. Returns a GD::Graph::bars.

See ~/dw/bin/dev/graphs_usage.pl for more detailed usage information.

=cut

sub bar2 {
    my ( $ref, $xlabel, $ylabel, $names_ref, $config_filename ) = @_;

    #Default settings (can be over-ridden by config file)
    my %settings = (
        x_label      => "\r\n$xlabel",
        y_label      => $ylabel,
        show_values  => 1,
        values_space => 1,               # Pixels between top of bar and value above
        b_margin     => 20,              # Bottom margin (makes space for labels)
        t_margin     => 50,              # Top margin - makes space for value above highest bar
        y_min_value  => 0.0,             # Stop scale going below zero

        legend_placement => 'RC',        # Right centre
        cumulate         => 'true',      # Put the two datasets in one bar
        bar_spacing      => undef,

        shadowclr   => $background,
        bgclr       => $background,
        fgclr       => 'white',
        boxclr      => '#f4eedc',        # Shaded-in background
        long_ticks  => 1,                # Background grid lines
        accentclr   => 'white',          # Colour of grid lines
        transparent => 0,                # 1 for transparent background

        labelclr     => '#000000',
        axislabelclr => '#000000',
        legendclr    => '#000000',
        valuesclr    => '#000000',
        textclr      => $textclr,
        dclrs        => $clrs,
    );
    my $image_width  = 500;              # Image width in pixels - can be over-ridden
    my $image_height = 350;

    # If there is a config file, get any settings from it
    if ( defined $config_filename ) {
        my $config = YAML::LoadFile("$LJ::HOME/etc/$config_filename");

        # Image size
        $image_width = $config->{image_width}
            if defined $config->{image_width};
        $image_height = $config->{image_height}
            if defined $config->{image_height};

        # Over-ride %settings with settings in config file, if they exist
        foreach my $k ( keys %settings ) {
            $settings{$k} = $config->{$k}
                if defined $config->{$k};
        }

        # Over-ride %fonts with font settings in config file, if they exist
        my $config_fonts = $config->{fonts};
        if ( defined $config_fonts ) {
            $fonts{$_} = $config_fonts->{$_} foreach keys %$config_fonts;
        }
    }

    # Create graph object
    my $graph = GD::Graph::bars->new( $image_width, $image_height );
    $graph->set(%settings) or die $graph->error;

    # Fonts defined at top in %fonts, and can be over-ridden by config file
    $graph->set_title_font( $fonts{font}, $fonts{title_size} );
    $graph->set_x_label_font( $fonts{font}, $fonts{label_size} );
    $graph->set_y_label_font( $fonts{font}, $fonts{label_size} );
    $graph->set_x_axis_font( $fonts{font}, $fonts{axis_size} );
    $graph->set_y_axis_font( $fonts{font}, $fonts{axis_size} );
    $graph->set_values_font( $fonts{font}, $fonts{value_size} );
    $graph->set_legend_font( $fonts{font}, $fonts{legend_size} );

    # Set legend
    $graph->set_legend(@$names_ref);

    # Make the graph
    my $gd = $graph->plot($ref) or die $graph->error;
    return $gd;
}

=head2 C<< DW::Graphs::bar2( $values_ref, $labels_ref, $xlabel, $ylabel [, $config_filename ] ) >>

Creates line graph with two or more sets of data from $ref ([ [ @value_labels ],
[ @dataset1 ], [ @dataset2 ], ... ]), $xlabel, $ylabel, $names_ref (label
arrayref, must have 1 element per dataset), using optional $config_filename to
override defaults. Returns a GD::Graph::bars.

See ~/dw/bin/dev/graphs_usage.pl for more detailed usage information.

=cut

sub lines {
    my ( $data_ref, $xlabel, $ylabel, $data_names, $config_filename ) = @_;

    #Default settings (can be over-ridden by config file)
    my %settings = (
        x_label          => $xlabel,
        y_label          => $ylabel,
        show_values      => 0,
        transparent      => 0,         # Set this to 1 for transparent background
        line_width       => 1,
        long_ticks       => 1,         # Background grid lines
        line_width       => 4,         # Line width in pixels
        legend_placement => 'RC',      # Right centre

        bgclr     => $background,
        fgclr     => 'white',
        boxclr    => '#f4eedc',        # Shaded-in background colour
        accentclr => 'lgray',

        labelclr     => '#000000',
        axislabelclr => '#000000',
        legendclr    => '#000000',
        valuesclr    => '#000000',
        textclr      => $textclr,
        dclrs        => $dark_clrs,
    );
    my $image_width  = 750;            # Image width in pixels - can be over-ridden
    my $image_height = 320;

    # If there is a config file, get any settings from it
    if ( defined $config_filename ) {
        my $config = YAML::LoadFile("$LJ::HOME/etc/$config_filename");

        # Image size
        $image_width = $config->{image_width}
            if defined $config->{image_width};
        $image_height = $config->{image_height}
            if defined $config->{image_height};

        # Over-ride %settings with settings in config file, if they exist
        foreach my $k ( keys %settings ) {
            $settings{$k} = $config->{$k}
                if defined $config->{$k};
        }

        # Over-ride %fonts with font settings in config file, if they exist
        my $config_fonts = $config->{fonts};
        if ( defined $config_fonts ) {
            $fonts{$_} = $config_fonts->{$_} foreach keys %$config_fonts;
        }
    }

    # Create Graph
    my $graph = GD::Graph::lines->new( $image_width, $image_height );
    $graph->set(%settings) or die $graph->error;
    $graph->set( line_types => [ 1, 2, 3, 4 ] );    # 1:solid 2:dash 3:dot 4:dot-dash
    $graph->set_legend(@$data_names);

    # Fonts defined at top in %fonts, and can be over-ridden by config file
    $graph->set_title_font( $fonts{font}, $fonts{title_size} );
    $graph->set_x_label_font( $fonts{font}, $fonts{label_size} );
    $graph->set_y_label_font( $fonts{font}, $fonts{label_size} );
    $graph->set_x_axis_font( $fonts{font}, $fonts{axis_size} );
    $graph->set_y_axis_font( $fonts{font}, $fonts{axis_size} );
    $graph->set_values_font( $fonts{font}, $fonts{value_size} );
    $graph->set_legend_font( $fonts{font}, $fonts{legend_size} );

    # Make the plot
    my $gd = $graph->plot($data_ref) or die $graph->error;
    return $gd;
}

1;

=head1 AUTHORS AND COPYRIGHT

Authors: Anarres <anarres@dreamwidth.org>

Copyright (c) 2010-2011 by Dreamwidth Studios, LLC.

This program is free software; you may redistribute it and/or modify it under
the same terms as Perl itself. For a copy of the license, please reference
'perldoc perlartistic' or 'perldoc perlgpl'.
