summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Chart.pm
diff options
context:
space:
mode:
authorgerv%gerv.net <>2003-06-26 08:22:50 +0200
committergerv%gerv.net <>2003-06-26 08:22:50 +0200
commit4f6b75a65628b0d86c760309dd81dd03f5c6d308 (patch)
tree5363459b06e75bc620ceab9dfd045b0f66b6c27c /Bugzilla/Chart.pm
parentda6143f4aae6af35f60b8230b82f649b3b0cbd05 (diff)
downloadbugzilla-4f6b75a65628b0d86c760309dd81dd03f5c6d308.tar.gz
bugzilla-4f6b75a65628b0d86c760309dd81dd03f5c6d308.tar.xz
Bug 16009 - generic charting. Patch by gerv; r,a=justdave.
Diffstat (limited to 'Bugzilla/Chart.pm')
-rw-r--r--Bugzilla/Chart.pm351
1 files changed, 351 insertions, 0 deletions
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 <gerv@gerv.net>
+
+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...
+ # &gt=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 "<pre>Bugzilla::Chart object:\n";
+ print Data::Dumper::Dumper($self);
+ print "</pre>";
+}
+
+1;