blob: badc95e7d0400ae3443be378efbd8ef832210018 [file] [log] [blame]
<?php
/**
* *****************************************************************************
* Copyright (c) 2016, 2017 Eclipse Foundation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
* *****************************************************************************
*/
/*
* This file defines some functions for generating various sorts of
* charts using the Google Charts API. When this file is loaded, it
* adds the necessary JavaScript to pull in the APIs to the page. The
* individual functions provide relatively high-level APIs for
* generating content.
*
* The $App variable must be defined before this file is included or
* required.
*/
require_once dirname ( __FILE__ ) . "/../classes/Project.class.php";
require_once dirname ( __FILE__ ) . '/../classes/database.inc';
class ChartContext {
var $start, $end;
function __construct($age = 5) {
$this->start = new DateTime('first day of this month');
$this->start->sub(new DateInterval("P{$age}Y"));
$this->end = new DateTime('last day of previous month');
}
function getCurrent() {
return $this->getEnd();
}
function getStart() {
return clone $this->start;
}
function getEnd() {
return clone $this->end;
}
/**
* Convenience function to create a date from an expression.
* Primarily, this function exists because you can't dispatch
* a message to the result of calling a constructor, which makes
* inlining a date expression impossible.
*
* e.g. $context->getDateTime('sunday last week')->format('Y-m-d');
*
* @param string $expression
* @return DateTime
*/
function getDateTime($expression) {
return new DateTime($expression);
}
/**
* Get information regarding the last complete quarter before
* the a particular date.
*
* @internal
* @param int $date UNIX date
* @return Quarter
*/
private function getLastQuarter($date) {
$year = date ( 'Y', $date );
$current = ceil ( date ( 'n', $date ) / 3 );
$last = $current - 1;
if ($last < 1) {
$year --;
$last = 4;
}
$start = str_pad ( $last * 3 - 2, 2, '0', STR_PAD_LEFT );
$end = str_pad ( $last * 3, 2, '0', STR_PAD_LEFT );
return new Quarter("${year}Q${last}",new DateTime("${year}${start}"),
"${year}${end}"
);
}
}
/**
* This helper function turns a string expressing a year/month
* combination (e.g. '2017-03' or '201703') into a more human readable
* form (e.g. 'March 2017').
*
* @param string $period
* @return string
*/
function asYearMonth($period) {
// TODO This function probably belongs in a different file.
if (! preg_match ( '/(\d\d\d\d)\-?(\d\d)/', $period, $matches ))
return $period;
$date = strtotime ( $matches [1] . '-' . $matches [2] . '-01' );
return date ( 'M Y', $date );
}
/**
* Get information regarding the quarter that occurs before the
* provided date. The returned value is an array that includes
* the name of the quarter (e.g. '2017Q1'), and the start and
* end year/month (e.g. '201701'). This format is very particular
* to the specific implementation of database tables that represent
* queries for commit statics (i.e. this is probably not a
* very generally-useable function).
*
* @internal
* @param int $date UNIX date
* @return string[]
*/
function getLastQuarter($date) {
// TODO This function probably belongs in a different file.
$year = date ( 'Y', $date );
$current = ceil ( date ( 'n', $date ) / 3 );
$last = $current - 1;
if ($last < 1) {
$year --;
$last = 4;
}
$start = str_pad ( $last * 3 - 2, 2, '0', STR_PAD_LEFT );
$end = str_pad ( $last * 3, 2, '0', STR_PAD_LEFT );
return array (
"${year}Q${last}",
"${year}${start}",
"${year}${end}"
);
}
/**
* Draw a pie chart. This function emits PHP code directly into
* the output stream and adds a bit of JavaScript into the page header.
* By default, the chart consumes 100% of the available width and
* most of the available height. These options can be overridden
* or augmented.
*
* <p>The global $App variable must be defined.
*
* <pre>
* $columns = array (
* array (
* 'label' => 'Project',
* 'type' => 'string'
* ),
* array (
* 'label' => 'CQs',
* 'type' => 'number'
* )
* );
* $values = array(
* array('Dash', 14),
* array('Equinox', 5)
* );
* drawPieChart('Projects', 'Projects and Numbers', $columns, $rows);
* </pre>
*
* @param string $id div identifer. Must be unique on the page
* @param string $title Title rendered at the top of the chart
* @param mixed $columns Array with name and type of columns
* @param mixed $values Array of arrays with values to render
* @param array $options Optional options.
*/
function drawPieChart($id, $title, $columns, $values, $options = array()) {
$defaultOptions = array (
'title' => $title,
'pieSliceText' => 'label',
'legend' => array('position' => 'labeled'),
'width' => 800,
'height' => 600,
'chartArea' => array('width' => '100%', 'top' => '10%', 'height'=>'85%')
);
$options = array_merge ( $defaultOptions, $options );
drawChart ( 'Pie', $id, $title, $columns, $values, $options );
}
/**
* Draw a line chart. This function emits PHP code directly into
* the output stream and adds a bit of JavaScript into the page header.
*
* The first column (and corresponding values) are used to label
* the horizontal axis. All other columns are values related to that
* label.
*
* <p>The global $App variable must be defined.
*
* <pre>
* $columns = array (
* array (
* 'label' => 'Month',
* 'type' => 'string'
* ),
* array (
* 'label' => 'Count',
* 'type' => 'number'
* )
* );
* $values = array(
* array('Dash', 14),
* array('Equinox', 5)
* );
* drawLineChart('Projects', 'Projects and Numbers', $columns, $rows);
* </pre>
*
* @param string $id div identifer. Must be unique on the page
* @param string $title Title rendered at the top of the chart
* @param mixed $columns Array with name and type of columns
* @param mixed $values Array of arrays with values to render
* @param array $options Optional options.
*/
function drawLineChart($id, $title, $columns, $values, $options = array()) {
$defaultOptions = array (
'title' => $title,
'width' => 800,
'height' => 600
);
$options = array_merge ( $defaultOptions, $options );
drawChart ( 'Line', $id, $title, $columns, $values, $options );
}
function drawBarChart($id, $title, $columns, $values, $options = array()) {
$defaultOptions = array (
'title' => $title,
'width' => 800,
'height' => 600
);
$options = array_merge ( $defaultOptions, $options );
drawChart ( 'Bar', $id, $title, $columns, $values, $options );
}
function drawChart($chartType, $id, $title, $columns, $values, $options) {
global $App;
$columnsJS = '';
foreach ( $columns as $definition ) {
$type = $definition ['type'];
$label = $definition ['label'];
$columnsJS .= "data.addColumn('$type', '$label');";
}
$valuesJSON = json_encode ( $values, JSON_NUMERIC_CHECK );
$optionsJSON = json_encode ( $options, JSON_NUMERIC_CHECK );
$js = "
google.setOnLoadCallback(draw${id}Chart);
function draw${id}Chart() {
// Create the data table.
var data = new google.visualization.DataTable();
$columnsJS;
data.addRows($valuesJSON);
// Set chart options
var options = $optionsJSON;
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.{$chartType}Chart(document.getElementById('${id}_div'));
chart.draw(data, options);
document.getElementById('${id}_div_png').innerHTML = '<a href=\"' + chart.getImageURI() + '\">Printable version</a>';
}";
$App->addExtraHtmlHeader ( "<script type=\"text/javascript\">$js</script>" );
echo "<div id=\"${id}_div_png\"></div>";
echo "<div id=\"${id}_div\"></div>";
}
/**
* This class implements a constructor pattern for building
* charts. All of the functions answer the receiver so that
* calls can be chained together. Most of the functions are
* concerned with configuring the chart; the
* <code>render()</code> renders the actual chart. Note that
* the rendering ends up being a combination of JavaScript in
* the header and some <code>&lt;div&gt;</code> tags in
* the content.
*
* <pre>ChartBuilder::named('chart_id')
* ->title('The Chart Title')
* ->query('dashboard', 'select field1, field2 from ...')
* ->column('Field One', 'field1', 'number')
* ->render();</pre>
*
*/
class ChartBuilder {
var $name;
var $title = '';
var $description = '';
var $dataFunction;
var $substitutions = array();
var $type = 'Line';
var $columns = array();
var $columnFields = array();
var $options = array();
var $titleString = "<h3 id=\":id\">:title</h3>";
public static function named($name) {
return new ChartBuilder($name);
}
private function __construct($name) {
$this->name = $name;
$this->options = array (
'width' => 800,
'height' => 600,
'legend' => array('position' => 'top'),
'chartArea' => array('width' => '80%', 'top' => '10%', 'bottom' => '10%', 'height'=>'80%')
);
}
public function title($title) {
$this->title = $title;
return $this;
}
public function description($description) {
$this->description = $description;
return $this;
}
public function dataFunction($callable) {
$this->dataFunction = $callable;
return $this;
}
public function query($database, $query) {
$receiver = $this;
$this->dataFunction = function() use (&$database, &$query, &$receiver) {
$rows = array();
query ( $database, $query, $this->substitutions, function ($row) use (&$receiver, &$rows) {
$values = array ();
foreach ( $receiver->columnFields as $field => $function ) {
$values [] = call_user_func( $function, $row [$field] );
}
$rows [] = $values;
} );
return $rows;
};
return $this;
}
public function substitute($key, $value) {
if ($value !== null)
$this->substitutions[$key] = $value;
return $this;
}
public function pieChart() {
$this->type = 'Pie';
$this->options = array_merge($this->options,
array (
'pieSliceText' => 'label',
'legend' => array('position' => 'labeled'),
'chartArea' => array('width' => '100%', 'top' => '10%', 'height'=>'85%')
));
return $this;
}
public function barChart() {
$this->type = 'Bar';
return $this;
}
public function columnChart() {
$this->type = 'Column';
return $this;
}
/**
* Provide information about a single column.
*
* @param string $label label for the column.
* @param string $field name of the field in the SQL query
* @param string $type type of the column, e.g. 'number' or 'string'
* @param callable $function Optional function to convert the field value before rendering in the chart.
*
* @return ChartBuilder The receiver.
*/
public function column($label, $field, $type = 'number', $function = null) {
$this->columns[] = array('label' => $label, 'type' => $type);
$this->columnFields[$field] = $function == null ? function($value) { return $value; } : $function;
return $this;
}
public function option($key, $value) {
$this->options[$key] = $value;
return $this;
}
public function titleString($value) {
$this->titleString = $value;
return $this;
}
public function height($value) {
return $this->option('height', $value);
}
public function width($value) {
return $this->option('width', $value);
}
/**
* Render the chart with the information contained in the receiver.
*/
public function render() {
$rows = call_user_func($this->dataFunction);
$options = array (
'curveType' => 'function',
'vAxis' => array (
'viewWindowMode' => 'explicit',
'viewWindow' => array (
'min' => 0
)
)
);
$options = array_merge($this->options, $options);
if ($this->title) {
$title = strtr($this->title, $this->substitutions);
echo strtr($this->titleString, array(':id' => $this->name, ':title' => $title));
}
if ($this->description) {
$description = strtr($this->description, $this->substitutions);
echo "<p>{$description}</p>";
}
drawChart ( $this->type, $this->name, $title, $this->columns, $rows, $options );
// $function = "draw{$this->type}Chart";
// $function ( $this->name, $title, $this->columns, $rows, $options );
}
}
$App->addExtraHtmlHeader ( "<script type=\"text/javascript\" src=\"https://www.google.com/jsapi\"></script>" );
$App->addExtraHtmlHeader ( "<script type=\"text/javascript\">google.load('visualization', '1.0', {'packages':['corechart']});</script>" );
?>