RSS Git Download  Clone
Raw Blame History
package LIMS::Controller::Chart;

use Moose;
BEGIN { extends 'LIMS::Base'; }
with 'LIMS::Controller::Roles::Chart'; # contains ChartDirector chart methods

use namespace::clean -except => 'meta';
__PACKAGE__->meta->make_immutable(inline_constructor => 0);

use Regexp::Common qw(number);
use LIMS::Local::Utils;
use Data::Dumper;

use lib '/home/raj/perl5/lib/perl5/ChartDirector';
use perlchartdir;

# ------------------------------------------------------------------------------
sub default : StartRunmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    # redirect to /resources:
    return $self->redirect( $self->query->url . '/resources' );
}

# ------------------------------------------------------------------------------
# just returns template, where name = param('id'):
sub load : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    
    my $template_name = $self->param('id')
    || $self->error( 'no template name passed to ' . $self->get_current_runmode );
    
    { # make some useful dates available to templates:
        $self->tt_params(
            start_date => $self->model('Request')->get_first_request_date(),
            today => DateTime->today(), 
        );
    }
    
    my $template = 'chart/' . $template_name . '.tt';    
    
    $self->render_view($template);
}

# ------------------------------------------------------------------------------
# target for /chart/process/<target_rm>/<chart_type> eg /chart/process/diagnoses/pie
sub process : Runmode {
    my $self = shift; my $rm = $self->get_current_runmode; $self->_debug_path($rm);
    
    # get target method (eg diagnoses):
    my $target = $self->param('id') || $self->stash->{chart_method}
    || return $self->error( 'no target passed to ' . $rm );
    
    # check it exists:
    unless ( $self->can($target) ) {
        return $self->error( "cannot find method for $target" );
    }
    
    # get chart_type from 2nd token in $self->param
    my $chart_type = $self->param('Id') || $self->stash->{chart_type}
    || return $self->error( 'no chart type passed to ' . $rm );    
    
    # check it exists:
    unless ( $self->can($chart_type) ) {
        return $self->error( "cannot find method for $chart_type" );
    }

    # create chart object in required method eg $self->diagnoses('pie_chart'):
    my $chart_obj = $self->$target($chart_type);
    
    # send to finalise_chart() to add title (if required) & run makeChart2():
    my $chart = $self->make_chart($chart_obj);
    
    # set header & return chart:
    $self->header_add(type => 'image/png');
    return $chart;
}

# ------------------------------------------------------------------------------
sub hiv : Runmode {
    my $self = shift; 
    
    # set header for png image:
    $self->header_add(type => 'image/png');

    my %args = (
        lab_section  => 'Flow cytometry',
        presentation => 'HIV', # screened as 
    );
    
    my $data = $self->_get_param_monitoring_data(\%args)
    || return $self->cleardot();

    my @chart_data; # create AoA:
    RESULT:
    for (@$data) { # AoH
        my $result = $_->{result};
        my @date = split '-', $_->{date}; # split into components for chartTime()
        
        # initialise all vars to zero:
        my $CD4_total = 0;
        
        my ($CD4pos)       = $result =~ /CD4\+ = (.*)\/uL/; # legacy
        my ($CD4posCD8neg) = $result =~ /CD4\+8\- T-cells = (.*) cells per microlitre/;
        my ($CD4posCD8pos) = $result =~ /CD4\+8\+ T-cells = (.*) cells per microlitre/;
        
        # make sure value not undef & remove any commas: 
        map {
            $_ ||= 0;
            $_ =~ s/,//; # dat supplied as eg 1,234
        } ($CD4pos, $CD4posCD8neg, $CD4posCD8pos);
        
        $CD4_total = $CD4posCD8neg + $CD4posCD8pos + $CD4pos;
        
        # skip unless have CD4 count:
        next RESULT unless $CD4_total; # warn Dumper $CD4_total;
        
        # add CD4 count & transformed date values:
        push @chart_data, [ $CD4_total, perlchartdir::chartTime(@date) ];
    } # warn Dumper \@chart_data;
    
    # stash data for Roles::Chart::_get_chart_data():
    $self->stash(
        chart_data  => \@chart_data,
        chart_title => undef, # don't need one
    ); 

    my $chart_obj = $self->plot_hiv();
    
    # send to finalise_chart() to add title (if required) & run makeChart2():
    my $chart = $self->make_chart($chart_obj); 
    return $chart;
}

# ------------------------------------------------------------------------------
sub pnh : Runmode {
    my $self = shift; 
    
    # set header for png image:
    $self->header_add(type => 'image/png');

    my %args = (
        lab_section  => 'Flow cytometry',
        presentation => 'PNH', # screened as 
    );
    
    my $data = $self->_get_param_monitoring_data(\%args)
    || return $self->cleardot();

    # integers & decimals: 
    my $re = qr(\d+|\d+\.\d+|\d+\.|\.\d+); # eg 100 | 99.99 | 90. | .1
    
    my @chart_data; # create AoA:
    RESULT:
    for (@$data) { # AoH
        my $result = $_->{result};
        my @date = split '-', $_->{date}; # split into components for chartTime()
        
        my ($granulocyte_data) = $result =~ /Granulocyte PNH clone = ($re)%/; 
        my ($erythrocyte_data) = $result =~ /Red cell PNH clone = ($re)%/; 
           # warn Dumper [\@date, $granulocyte_data, $erythrocyte_data];
            
        # skip unless have at least 1 result:
        next RESULT unless $granulocyte_data || $erythrocyte_data;
        
        # add granulocyte & red_cell clone data & transformed date values:
        my $pnh_data = [ $granulocyte_data, $erythrocyte_data ]; # deref'd in plot_pnh()
        push @chart_data, [ $pnh_data, perlchartdir::chartTime(@date) ];
    } # warn Dumper \@chart_data;
    
    # stash data for Roles::Chart::_get_chart_data():
    $self->stash(
        chart_data  => \@chart_data,
        chart_title => undef, # don't need one
    ); 

    my $chart_obj = $self->plot_pnh();
    
    # send to finalise_chart() to add title (if required) & run makeChart2():
    my $chart = $self->make_chart($chart_obj); 
    return $chart;
}

# ------------------------------------------------------------------------------
sub imatinib : Runmode {
    my $self = shift;
    
    # set header for png image:
    $self->header_add(type => 'image/png');

    my %args = (
        lab_section  => 'Molecular',
        presentation => 'CML imatinib PB follow-up', # screened as 
    );
    
    my $data = $self->_get_param_monitoring_data(\%args)
    || return $self->cleardot();

    my @chart_data; # create AoA:
    RESULT:
    for (@$data) { # AoH
        my $result = $_->{result};
        my @date = split '-', $_->{date}; # split into components for chartTime()
        
        my ($transcription_number) = $result =~ /transcription number = (\d+)/;
        my ($ratio)                = $result =~ /BCR-ABL : ABL ratio = (.*)%/;
        
        # skip unless ratio result captured:
        next RESULT unless $ratio; # warn Dumper $ratio;
        
        # convert any 'less-than' result into real number:
        if ($ratio =~ /\A<(.*)/) {
            # set $transcription_number to zero (if not already):
            $transcription_number = 0; # changes plot symbol
            $ratio = $1; # remove leading '<'
        }
        # skip if still not 'real' number (ie integer or decimal):
        elsif ( $ratio !~ /\A$RE{num}{real}\Z/ ) { # warn $ratio;
            # $ratio = $perlchartdir::NoValue; # probably 'see comment' or 'failed'
            next RESULT; # so skip
        }
        
        # set point_type (depends whether transcription_number > 0):
        my $point_type = $transcription_number ? 1 : 0;
            
        # add ratio, transformed date values, and point_type:
        push @chart_data, [ $ratio, perlchartdir::chartTime(@date), $point_type ];
    } # warn Dumper \@chart_data;

    # stash data for Roles::Chart::_get_chart_data():
    $self->stash(
        chart_data  => \@chart_data,
        chart_title => undef, # don't need one
    ); 

    my $chart_obj = $self->plot_imatinib();

    # send to finalise_chart() to add title (if required) & run makeChart2():
    my $chart = $self->make_chart($chart_obj);
    return $chart;
}

# ------------------------------------------------------------------------------
# receives chart object, adds title (if required) & runs makeChart2():
sub make_chart {
    my $self  = shift;
    my $chart = shift; 
    
    # add a (optional) title to the chart:
    if ( my $title = $self->stash->{chart_title} ) {
        $chart->addTitle($title);
    }
    
    return $chart->makeChart2($perlchartdir::PNG);    
}

# ------------------------------------------------------------------------------
sub diagnosis_status {
    my ($self, $chart_type) = @_; $self->_debug_path('diagnosis_status');
    
    my $vars = $self->query->Vars; # $self->debug($vars);
    
    my $title = $self->_get_chart_title($vars) || 'previous 365 days';
    
    my $data = $self->model('Chart')->get_diagnosis_status($vars); # $self->debug($data);
    
    # stash title & data for create_chart():
    $self->stash(
        chart_title => $title,
        chart_data  => $data,
    ); 

    my $chart = $self->$chart_type();
    
    return $chart;
}

# ------------------------------------------------------------------------------
sub diagnosis_frequency {
    my ($self, $chart_type) = @_; $self->_debug_path('diagnosis_frequency');
    
    my $vars = $self->query->Vars; # $self->debug($vars);
    
    my $title = $self->_get_chart_title($vars) || 'previous 365 days';
    
    my $data = $self->model('Chart')->get_diagnosis_frequency($vars); # $self->debug($data);
    
    # stash title & data for create_chart():
    $self->stash(
        chart_title => $title,
        chart_data  => $data,
    ); 

    my $chart = $self->$chart_type();
    
    return $chart;
}

# ------------------------------------------------------------------------------
sub requests_by_month {
    my ($self, $chart_type) = @_; $self->_debug_path('diagnosis_frequency');
    
    my $vars = $self->query->Vars; # $self->debug($vars);
    
    my $title = $self->_get_chart_title($vars) || 'previous 365 days';
    
    my $data = $self->model('Chart')->requests_by_month($vars); # $self->debug($data);
    
    # stash title & data for create_chart():
    $self->stash(
        y_axis_label => 'requests',
        chart_title  => $title,
        chart_data   => $data,
    ); 

    my $chart = $self->$chart_type();
    
    return $chart;
}

# ------------------------------------------------------------------------------
sub requests_by_day_of_week {
    my ($self, $chart_type) = @_; $self->_debug_path('requests_by_day_of_week');
    
    my $vars = $self->query->Vars; # $self->debug($vars);
    
    my $title = $self->_get_chart_title($vars) || 'previous 365 days';
    
    my $data = $self->model('Chart')->requests_by_day_of_week($vars); # $self->debug($data);
    
    # stash title,data & y_axis_label (if required) for create_chart():
    $self->stash(
        y_axis_label => 'requests',
        chart_title  => $title,
        chart_data   => $data,
    ); 

    my $chart = $self->$chart_type();
    
    return $chart;
}

# ------------------------------------------------------------------------------
sub type_one {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
   
    # The data for the pie chart
    my $data = [25, 18, 15, 12, 8, 30, 35];
    
    # The labels for the pie chart
    my $labels = [ qw(CML DLBCL AML MDS CMPD Myeloma ALL) ];
    
    # Create a PieChart object of size 360 x 300 pixels
    my $c = new PieChart(360, 300);
    
    # Set the center of the pie at (180, 140) and the radius to 100 pixels
    $c->setPieSize(180, 140, 100);
    
    # Add a title to the pie chart
    $c->addTitle('Diagnosis Frequency 2009');
    
    # Draw the pie in 3D
    $c->set3D();
    
    # Set the pie data and the pie labels
    $c->setData($data, $labels);
    
    # Explode the 1st sector (index = 0)
    $c->setExplode(0);    
    
    return $c;   
}

sub type_three {
    my $self = shift;
    
    # The data for the pie chart
    my $data = [13, 16, 42];
    
    # The labels for the pie chart
    my $labels = [ qw(New Relapse FollowUp) ];
    
    # The colors to use for the sectors
    my $colors = [0x66ff66, 0xff6666, 0xffff00];
    
    # Create a PieChart object of size 300 x 300 pixels. Set the background to a gradient
    # color from blue (aaccff) to sky blue (ffffff), with a grey (888888) border. Use
    # rounded corners and soft drop shadow.
    my $c = new PieChart(300, 300);
    $c->setBackground($c->linearGradientColor(0, 0, 0, $c->getHeight() / 2, 0xaaccff,
        0xffffff), 0x888888);
    $c->setRoundedFrame();
    $c->setDropShadow();
    
    if ( $self->query->param('foo') ) {
    #============================================================
    #    Draw a pie chart where the label is on top of the pie
    #============================================================
    
        # Set the center of the pie at (150, 150) and the radius to 120 pixels
        $c->setPieSize(150, 150, 120);
    
        # Set the label position to -40 pixels from the perimeter of the pie (-ve means
        # label is inside the pie)
        $c->setLabelPos(-40);
    
    } else {
    #============================================================
    #    Draw a pie chart where the label is outside the pie
    #============================================================
    
        # Set the center of the pie at (150, 150) and the radius to 80 pixels
        $c->setPieSize(150, 150, 80);
    
        # Set the sector label position to be 20 pixels from the pie. Use a join line to
        # connect the labels to the sectors.
        $c->setLabelPos(20, $perlchartdir::LineColor);
    
    }
    
    # Add a title to the pie chart
    $c->addTitle('Diagnosis Status 2009');

    # Set the pie data and the pie labels
    $c->setData($data, $labels);
    
    # Set the sector colors
    $c->setColors2($perlchartdir::DataColor, $colors);
    
    # Use local gradient shading, with a 1 pixel semi-transparent black (bb000000) border
    $c->setSectorStyle($perlchartdir::LocalGradientShading, 0xbb000000, 1);
    
    return $c;        
}

sub type_four {
    my $self = shift;
   
    # The value to display on the meter
    my $value = 27.48;
    
    # Create an AngularMeter object of size 200 x 115 pixels, with silver background
    # color, black border, 2 pixel 3D border border and rounded corners
    my $m = new AngularMeter(200, 115, perlchartdir::silverColor(), 0x000000, 2);
    $m->setRoundedFrame();
    
    # Set the meter center at (100, 100), with radius 85 pixels, and span from -90 to +90
    # degress (semi-circle)
    $m->setMeter(100, 100, 85, -90, 90);
    
    # Meter scale is 0 - 100, with major tick every 20 units, minor tick every 10 units,
    # and micro tick every 5 units
    $m->setScale(0, 100, 20, 10, 5);
    
    # Set 0 - 60 as green (66FF66) zone
    $m->addZone(0, 60, 0, 85, 0x66ff66);
    
    # Set 60 - 80 as yellow (FFFF33) zone
    $m->addZone(60, 80, 0, 85, 0xffff33);
    
    # Set 80 - 100 as red (FF6666) zone
    $m->addZone(80, 100, 0, 85, 0xff6666);
    
    # Add a text label centered at (100, 60) with 12 pts Arial Bold font
    $m->addText(100, 60, "Workload (% max)", "arialbd.ttf", 12, $perlchartdir::TextColor,
        $perlchartdir::Center);
    
    # Add a text box at the top right corner of the meter showing the value formatted to
    # 2 decimal places, using white text on a black background, and with 1 pixel 3D
    # depressed border
    $m->addText(156, 8, $m->formatValue($value, "2"), "arial.ttf", 8, 0xffffff
        )->setBackground(0x000000, 0, -1);
    
    # Add a semi-transparent blue (40666699) pointer with black border at the specified
    # value
    $m->addPointer($value, 0x40666699, 0x000000);
 
    return $m;     
}

sub diagnosis {
    my $self = shift;
    
    my $d = $self->model('Chart')->get_diagnosis_frequency(); # $self->debug($d);

#    my (@terms, @count);
#    while ( my ($i, $term) = each %$d ) {
#        push @count, $i;
#        push @terms, $term;
#    }

    my @count = map $_->[0], @$d;
    my @terms = map $_->[1], @$d;
    
    # The data for the pie chart
    my $data = \@count;

    # The labels for the pie chart
    my $labels = \@terms;

    # Create a PieChart object of size 560 x 270 pixels, with a golden background and a 1
    # pixel 3D border
    my $c = new PieChart(1000, 400, perlchartdir::goldColor(), -1, 1);
    
    # Add a title box using 15 pts Times Bold Italic font and metallic pink background
    # color
    $c->addTitle("Diagnosis Frequency 2009 (top 15)", "timesbi.ttf", 15)
        ->setBackground( perlchartdir::metalColor(0xff9999) );
    
    # Set the center of the pie at (280, 135) and the radius to 110 pixels
    $c->setPieSize(500, 200, 150);
    
    # Draw the pie in 3D with 20 pixels 3D depth
    $c->set3D(20);
    
    # Use the side label layout method
    $c->setLabelLayout($perlchartdir::SideLayout);
    
    # Set the label box background color the same as the sector color, with glass effect,
    # and with 5 pixels rounded corners
    my $t = $c->setLabelStyle();
    $t->setBackground($perlchartdir::SameAsMainColor, $perlchartdir::Transparent,
        perlchartdir::glassEffect());
    #$t->setRoundedCorners(5);
    
    # Set the border color of the sector the same color as the fill color. Set the line
    # color of the join line to black (0x0)
    $c->setLineColor($perlchartdir::SameAsMainColor, 0x000000);
    
    # Set the start angle to 135 degrees may improve layout when there are many small
    # sectors at the end of the data array (that is, data sorted in descending order). It
    # is because this makes the small sectors position near the horizontal axis, where
    # the text label has the least tendency to overlap. For data sorted in ascending
    # order, a start angle of 45 degrees can be used instead.
    $c->setStartAngle(135);
    
    # Set the pie data and the pie labels
    $c->setData($data, $labels);
    
    return $c;
}

# ------------------------------------------------------------------------------
sub _get_param_monitoring_data { # eg HIV/Imatinib:
    my ($self, $args) = @_; # arrayref
    
    my $request_id = $self->param('id') # already checked in tmpl
    || return 0; 

    my $request
        = $self->model('Request')->get_patient_and_request_data($request_id);
    # stash for later:
    $self->stash( request_data => $request->as_tree );    
    
    { # need at least 1 previous dataset to plot graph:
        my %h = ( request => $request, screen => $args->{presentation} );
        # returns true if any previous data:
        $self->model('Local')->has_previous_data(\%h) || return 0;
    }
    
    { # get previous data on same patient:
        my $patient_id = $request->patient_case->patient_id;
        $args->{patient_id} = $patient_id; # add patient_id to args
    }
    
    my $data = $self->model('Chart')->get_param_monitoring_data($args); # $self->debug($data);
    return $data || 0; 
}

# ------------------------------------------------------------------------------
sub _get_chart_title {
    my $self = shift;
    my $vars = shift;
    
    my $constraint_type = $vars->{constraint_type} || return; # undef on 1st call
    
    my $title;
    
    # uses same logic as Model::Chart::_get_search_constraints() to match search
    # constraints:
    if ($constraint_type eq 'all_data') {
        $title = 'all requests';
    }
    elsif ($constraint_type eq 'this_year') {
        $title = 'year ' . DateTime->now->year;
    }
    elsif ($vars->{days}) {
        $title = "previous $vars->{days} days"
    }
    elsif ($vars->{year}) {
        $title = "year $vars->{year}"
    }
    # ok to use $constraint_type as both dates validated before title set, if
    # invalid search constraints default to previous 365 days & default title used
    elsif ($constraint_type eq 'date_range') {
        # check they're both valid:
        my $start_date = LIMS::Local::Utils::check_date($vars->{date_from});
        my $end_date   = LIMS::Local::Utils::check_date($vars->{date_to});        
        # set title if both dates valid:
        if ($start_date && $end_date) {
            $title = "between $vars->{date_from} & $vars->{date_to}";
        }
    }
    
    return $title;
}

1;