From 4f6b75a65628b0d86c760309dd81dd03f5c6d308 Mon Sep 17 00:00:00 2001 From: "gerv%gerv.net" <> Date: Thu, 26 Jun 2003 06:22:50 +0000 Subject: Bug 16009 - generic charting. Patch by gerv; r,a=justdave. --- Bugzilla/Chart.pm | 351 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 Bugzilla/Chart.pm (limited to 'Bugzilla/Chart.pm') diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm new file mode 100644 index 000000000..03b5e4173 --- /dev/null +++ b/Bugzilla/Chart.pm @@ -0,0 +1,351 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Gervase Markham + +use strict; +use lib "."; + +# This module represents a chart. +# +# Note that it is perfectly legal for the 'lines' member variable of this +# class (which is an array of Bugzilla::Series objects) to have empty members +# in it. If this is true, the 'labels' array will also have empty members at +# the same points. +package Bugzilla::Chart; + +use Bugzilla::Util; +use Bugzilla::Series; + +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + # Create a ref to an empty hash and bless it + my $self = {}; + bless($self, $class); + + if ($#_ == 0) { + # Construct from a CGI object. + $self->init($_[0]); + } + else { + die("CGI object not passed in - invalid number of args \($#_\)($_)"); + } + + return $self; +} + +sub init { + my $self = shift; + my $cgi = shift; + + # The data structure is a list of lists (lines) of Series objects. + # There is a separate list for the labels. + # + # The URL encoding is: + # line0=67&line0=73&line1=81&line2=67... + # &label0=B+/+R+/+NEW&label1=... + # &select0=1&select3=1... + # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... + # >=1&labelgt=Grand+Total + foreach my $param ($cgi->param()) { + # Store all the lines + if ($param =~ /^line(\d+)$/) { + foreach my $series_id ($cgi->param($param)) { + detaint_natural($series_id) + || &::ThrowCodeError("invalid_series_id"); + push(@{$self->{'lines'}[$1]}, + new Bugzilla::Series($series_id)); + } + } + + # Store all the labels + if ($param =~ /^label(\d+)$/) { + $self->{'labels'}[$1] = $cgi->param($param); + } + } + + # Store the miscellaneous metadata + $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; + $self->{'gt'} = $cgi->param('gt') ? 1 : 0; + $self->{'labelgt'} = $cgi->param('labelgt'); + $self->{'datefrom'} = $cgi->param('datefrom'); + $self->{'dateto'} = $cgi->param('dateto'); + + # Make sure the dates are ones we are able to interpret + foreach my $date ('datefrom', 'dateto') { + if ($self->{$date}) { + $self->{$date} = &::str2time($self->{$date}) + || ThrowUserError("illegal_date", { date => $self->{$date}}); + } + } + + # datefrom can't be after dateto + if ($self->{'datefrom'} && $self->{'dateto'} && + $self->{'datefrom'} > $self->{'dateto'}) + { + &::ThrowUserError("misarranged_dates", + {'datefrom' => $cgi->param('datefrom'), + 'dateto' => $cgi->param('dateto')}); + } +} + +# Alter Chart so that the selected series are added to it. +sub add { + my $self = shift; + my @series_ids = @_; + + # If we are going from < 2 to >= 2 series, add the Grand Total line. + if (!$self->{'gt'}) { + my $current_size = scalar($self->getSeriesIDs()); + if ($current_size < 2 && + $current_size + scalar(@series_ids) >= 2) + { + $self->{'gt'} = 1; + } + } + + # Create new Series and push them on to the list of lines. + # Note that new lines have no label; the display template is responsible + # for inventing something sensible. + foreach my $series_id (@series_ids) { + my $series = new Bugzilla::Series($series_id); + push(@{$self->{'lines'}}, [$series]); + push(@{$self->{'labels'}}, ""); + } +} + +# Alter Chart so that the selections are removed from it. +sub remove { + my $self = shift; + my @line_ids = @_; + + foreach my $line_id (@line_ids) { + if ($line_id == 65536) { + # Magic value - delete Grand Total. + $self->{'gt'} = 0; + } + else { + delete($self->{'lines'}->[$line_id]); + delete($self->{'labels'}->[$line_id]); + } + } +} + +# Alter Chart so that the selections are summed. +sub sum { + my $self = shift; + my @line_ids = @_; + + # We can't add the Grand Total to things. + @line_ids = grep(!/^65536$/, @line_ids); + + # We can't add less than two things. + return if scalar(@line_ids) < 2; + + my @series; + my $label = ""; + my $biggestlength = 0; + + # We rescue the Series objects of all the series involved in the sum. + foreach my $line_id (@line_ids) { + my @line = @{$self->{'lines'}->[$line_id]}; + + foreach my $series (@line) { + push(@series, $series); + } + + # We keep the label that labels the line with the most series. + if (scalar(@line) > $biggestlength) { + $biggestlength = scalar(@line); + $label = $self->{'labels'}->[$line_id]; + } + } + + $self->remove(@line_ids); + + push(@{$self->{'lines'}}, \@series); + push(@{$self->{'labels'}}, $label); +} + +sub data { + my $self = shift; + $self->{'_data'} ||= $self->readData(); + return $self->{'_data'}; +} + +# Convert the Chart's data into a plottable form in $self->{'_data'}. +sub readData { + my $self = shift; + my @data; + + my $series_ids = join(",", $self->getSeriesIDs()); + + # Work out the date boundaries for our data. + my $dbh = Bugzilla->dbh; + + # The date used is the one given if it's in a sensible range; otherwise, + # it's the earliest or latest date in the database as appropriate. + my $datefrom = $dbh->selectrow_array("SELECT MIN(date) FROM series_data " . + "WHERE series_id IN ($series_ids)"); + $datefrom = &::str2time($datefrom); + + if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { + $datefrom = $self->{'datefrom'}; + } + + my $dateto = $dbh->selectrow_array("SELECT MAX(date) FROM series_data " . + "WHERE series_id IN ($series_ids)"); + $dateto = &::str2time($dateto); + + if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { + $dateto = $self->{'dateto'}; + } + + # Prepare the query which retrieves the data for each series + my $query = "SELECT TO_DAYS(date) - TO_DAYS(FROM_UNIXTIME($datefrom)), " . + "value FROM series_data " . + "WHERE series_id = ? " . + "AND date >= FROM_UNIXTIME($datefrom)"; + if ($dateto) { + $query .= " AND date <= FROM_UNIXTIME($dateto)"; + } + + my $sth = $dbh->prepare($query); + + my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; + my $line_index = 0; + + foreach my $line (@{$self->{'lines'}}) { + # Even if we end up with no data, we need an empty arrayref to prevent + # errors in the PNG-generating code + $data[$line_index] = []; + + foreach my $series (@$line) { + + # Get the data for this series and add it on + $sth->execute($series->{'series_id'}); + my $points = $sth->fetchall_arrayref(); + + foreach my $point (@$points) { + my ($datediff, $value) = @$point; + $data[$line_index][$datediff] ||= 0; + $data[$line_index][$datediff] += $value; + + # Add to the grand total, if we are doing that + if ($gt_index) { + $data[$gt_index][$datediff] ||= 0; + $data[$gt_index][$datediff] += $value; + } + } + } + + $line_index++; + } + + # Add the x-axis labels into the data structure + my $date_progression = generateDateProgression($datefrom, $dateto); + unshift(@data, $date_progression); + + if ($self->{'gt'}) { + # Add Grand Total to label list + push(@{$self->{'labels'}}, $self->{'labelgt'}); + + $data[$gt_index] ||= []; + } + + return \@data; +} + +# Flatten the data structure into a list of series_ids +sub getSeriesIDs { + my $self = shift; + my @series_ids; + + foreach my $line (@{$self->{'lines'}}) { + foreach my $series (@$line) { + push(@series_ids, $series->{'series_id'}); + } + } + + return @series_ids; +} + +# Class method to get the data necessary to populate the "select series" +# widgets on various pages. +sub getVisibleSeries { + my %cats; + + # Get all visible series + my $dbh = Bugzilla->dbh; + my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " . + "series.name, series.series_id " . + "FROM series " . + "LEFT JOIN series_categories AS cc1 " . + " ON series.category = cc1.category_id " . + "LEFT JOIN series_categories AS cc2 " . + " ON series.subcategory = cc2.category_id " . + "LEFT JOIN user_series_map AS ucm " . + " ON series.series_id = ucm.series_id " . + "WHERE ucm.user_id = 0 OR ucm.user_id = $::userid"); + + foreach my $series (@$serieses) { + my ($cat, $subcat, $name, $series_id) = @$series; + $cats{$cat}{$subcat}{$name} = $series_id; + } + + return \%cats; +} + +sub generateDateProgression { + my ($datefrom, $dateto) = @_; + my @progression; + + $dateto = $dateto || time(); + my $oneday = 60 * 60 * 24; + + # When the from and to dates are converted by str2time(), you end up with + # a time figure representing midnight at the beginning of that day. We + # adjust the times by 1/3 and 2/3 of a day respectively to prevent + # edge conditions in time2str(). + $datefrom += $oneday / 3; + $dateto += (2 * $oneday) / 3; + + while ($datefrom < $dateto) { + push (@progression, &::time2str("%Y-%m-%d", $datefrom)); + $datefrom += $oneday; + } + + return \@progression; +} + +sub dump { + my $self = shift; + + # Make sure we've read in our data + my $data = $self->data; + + require Data::Dumper; + print "
Bugzilla::Chart object:\n";
+    print Data::Dumper::Dumper($self);
+    print "
"; +} + +1; -- cgit v1.2.3-24-g4f1b