blob: 0c86e84574e946213d1c0d3d3ed49175c4b55bef [file] [log] [blame]
<?php
/**
* File containing the ezcGraphLineChart class
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Graph
* @version //autogentag//
* @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Class for line charts. Can make use of an unlimited amount of datasets and
* will display them as lines by default.
* X axis:
* - Labeled axis
* - Centered axis label renderer
* Y axis:
* - Numeric axis
* - Exact axis label renderer
*
* <code>
* // Create a new line chart
* $chart = new ezcGraphLineChart();
*
* // Add data to line chart
* $chart->data['sample dataset'] = new ezcGraphArrayDataSet(
* array(
* '100' => 1.2,
* '200' => 43.2,
* '300' => -34.14,
* '350' => 65,
* '400' => 123,
* )
* );
*
* // Render chart with default 2d renderer and default SVG driver
* $chart->render( 500, 200, 'line_chart.svg' );
* </code>
*
* Each chart consists of several chart elements which represents logical
* parts of the chart and can be formatted independently. The line chart
* consists of:
* - title ( {@link ezcGraphChartElementText} )
* - legend ( {@link ezcGraphChartElementLegend} )
* - background ( {@link ezcGraphChartElementBackground} )
* - xAxis ( {@link ezcGraphChartElementLabeledAxis} )
* - yAxis ( {@link ezcGraphChartElementNumericAxis} )
*
* The type of the axis may be changed and all elements can be configured by
* accessing them as properties of the chart:
*
* <code>
* $chart->legend->position = ezcGraph::RIGHT;
* </code>
*
* The chart itself also offers several options to configure the appearance.
* The extended configure options are available in
* {@link ezcGraphLineChartOptions} extending the {@link ezcGraphChartOptions}.
*
* @property ezcGraphLineChartOptions $options
* Chart options class
*
* @version //autogentag//
* @package Graph
* @mainclass
*/
class ezcGraphLineChart extends ezcGraphChart
{
/**
* Array with additional axis for the chart
*
* @var ezcGraphAxisContainer
*/
protected $additionalAxis;
/**
* Constructor
*
* @param array $options Default option array
* @return void
* @ignore
*/
public function __construct( array $options = array() )
{
$this->additionalAxis = new ezcGraphAxisContainer( $this );
$this->options = new ezcGraphLineChartOptions( $options );
$this->options->highlightFont = $this->options->font;
parent::__construct();
$this->addElement( 'xAxis', new ezcGraphChartElementLabeledAxis() );
$this->elements['xAxis']->position = ezcGraph::LEFT;
$this->addElement( 'yAxis', new ezcGraphChartElementNumericAxis() );
$this->elements['yAxis']->position = ezcGraph::BOTTOM;
}
/**
* __get
*
* @param mixed $propertyName
* @throws ezcBasePropertyNotFoundException
* If a the value for the property options is not an instance of
* @return mixed
* @ignore
*/
public function __get( $propertyName )
{
switch ( $propertyName )
{
case 'additionalAxis':
return $this->additionalAxis;
}
return parent::__get( $propertyName );
}
/**
* Options write access
*
* @throws ezcBasePropertyNotFoundException
* If Option could not be found
* @throws ezcBaseValueException
* If value is out of range
* @param mixed $propertyName Option name
* @param mixed $propertyValue Option value;
* @return mixed
* @ignore
*/
public function __set( $propertyName, $propertyValue )
{
switch ( $propertyName ) {
case 'xAxis':
if ( $propertyValue instanceof ezcGraphChartElementAxis )
{
$this->addElement( 'xAxis', $propertyValue );
$this->elements['xAxis']->position = ezcGraph::LEFT;
}
else
{
throw new ezcBaseValueException( $propertyName, $propertyValue, 'ezcGraphChartElementAxis' );
}
break;
case 'yAxis':
if ( $propertyValue instanceof ezcGraphChartElementAxis )
{
$this->addElement( 'yAxis', $propertyValue );
$this->elements['yAxis']->position = ezcGraph::BOTTOM;
}
else
{
throw new ezcBaseValueException( $propertyName, $propertyValue, 'ezcGraphChartElementAxis' );
}
break;
default:
parent::__set( $propertyName, $propertyValue );
}
}
/**
* Set colors and border for this element
*
* @param ezcGraphPalette $palette Palette
* @return void
*/
public function setFromPalette( ezcGraphPalette $palette )
{
foreach ( $this->additionalAxis as $element )
{
$element->setFromPalette( $palette );
}
parent::setFromPalette( $palette );
}
/**
* Calculate bar chart step width
*
* @return void
*/
protected function calculateStepWidth( ezcGraphChartElementAxis $mainAxis, ezcGraphChartElementAxis $secondAxis, $width )
{
$steps = $mainAxis->getSteps();
$stepWidth = null;
foreach ( $steps as $step )
{
if ( $stepWidth === null )
{
$stepWidth = $step->width;
}
elseif ( $step->width !== $stepWidth )
{
throw new ezcGraphUnregularStepsException();
}
}
$step = reset( $steps );
if ( count( $step->childs ) )
{
// Keep this for BC reasons
$barCount = ( $mainAxis->getMajorStepCount() + 1 ) * ( $mainAxis->getMinorStepCount() - 1 );
$stepWidth = 1 / $barCount;
}
$checkedRegularSteps = true;
return $mainAxis->axisLabelRenderer->modifyChartDataPosition(
$secondAxis->axisLabelRenderer->modifyChartDataPosition(
new ezcGraphCoordinate(
$width * $stepWidth,
$width * $stepWidth
)
)
);
}
/**
* Render the assigned data
*
* Will renderer all charts data in the remaining boundings after drawing
* all other chart elements. The data will be rendered depending on the
* settings in the dataset.
*
* @param ezcGraphRenderer $renderer Renderer
* @param ezcGraphBoundings $boundings Remaining boundings
* @return void
*/
protected function renderData( ezcGraphRenderer $renderer, ezcGraphBoundings $boundings, ezcGraphBoundings $innerBoundings )
{
// Use inner boundings for drawning chart data
$boundings = $innerBoundings;
$yAxisNullPosition = $this->elements['yAxis']->getCoordinate( false );
// Initialize counters
$nr = array();
$count = array();
foreach ( $this->data as $data )
{
if ( !isset( $nr[$data->displayType->default] ) )
{
$nr[$data->displayType->default] = 0;
$count[$data->displayType->default] = 0;
}
$nr[$data->displayType->default]++;
$count[$data->displayType->default]++;
}
$checkedRegularSteps = false;
// Display data
foreach ( $this->data as $datasetName => $data )
{
--$nr[$data->displayType->default];
// Check which axis should be used
$xAxis = ( $data->xAxis->default ? $data->xAxis->default: $this->elements['xAxis'] );
$yAxis = ( $data->yAxis->default ? $data->yAxis->default: $this->elements['yAxis'] );
// Determine fill color for dataset
if ( $this->options->fillLines !== false )
{
$fillColor = clone $data->color->default;
$fillColor->alpha = (int) round( ( 255 - $fillColor->alpha ) * ( $this->options->fillLines / 255 ) );
}
else
{
$fillColor = null;
}
// Ensure regular steps on axis when used with bar charts and
// precalculate some values use to render bar charts
//
// Called only once and only when bars should be rendered
if ( ( $checkedRegularSteps === false ) &&
( $data->displayType->default === ezcGraph::BAR ) )
{
$width = $this->calculateStepWidth( $xAxis, $yAxis, $boundings->width )->x;
}
// Draw lines for dataset
$lastPoint = false;
foreach ( $data as $key => $value )
{
// Calculate point in chart
$point = $xAxis->axisLabelRenderer->modifyChartDataPosition(
$yAxis->axisLabelRenderer->modifyChartDataPosition(
new ezcGraphCoordinate(
$xAxis->getCoordinate( $key ),
$yAxis->getCoordinate( $value )
)
)
);
// Render depending on display type of dataset
switch ( true )
{
case $data->displayType->default === ezcGraph::LINE:
$renderer->drawDataLine(
$boundings,
new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
$data->color->default,
( $lastPoint === false ? $point : $lastPoint ),
$point,
$nr[$data->displayType->default],
$count[$data->displayType->default],
$data->symbol[$key],
$data->color[$key],
$fillColor,
$yAxisNullPosition,
( $data->lineThickness->default ? $data->lineThickness->default : $this->options->lineThickness )
);
// Render highlight string if requested
if ( $data->highlight[$key] )
{
$renderer->drawDataHighlightText(
$boundings,
new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
$point,
$yAxisNullPosition,
$nr[$data->displayType->default],
$count[$data->displayType->default],
$this->options->highlightFont,
( $data->highlightValue[$key] ? $data->highlightValue[$key] : $value ),
$this->options->highlightSize + $this->options->highlightFont->padding * 2,
( $this->options->highlightLines ? $data->color[$key] : null ),
( $this->options->highlightXOffset ? $this->options->highlightXOffset : 0 ),
( $this->options->highlightYOffset ? $this->options->highlightYOffset : 0 ),
0.,
ezcGraph::LINE
);
}
break;
case ( $data->displayType->default === ezcGraph::BAR ) &&
$this->options->stackBars :
// Check if a bar has already been stacked
if ( !isset( $stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] ) )
{
$start = new ezcGraphCoordinate(
$point->x,
$yAxisNullPosition
);
$stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] = $value;
}
else
{
$start = $xAxis->axisLabelRenderer->modifyChartDataPosition(
$yAxis->axisLabelRenderer->modifyChartDataPosition(
new ezcGraphCoordinate(
$xAxis->getCoordinate( $key ),
$yAxis->getCoordinate( $stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] )
)
)
);
$point = $xAxis->axisLabelRenderer->modifyChartDataPosition(
$yAxis->axisLabelRenderer->modifyChartDataPosition(
new ezcGraphCoordinate(
$xAxis->getCoordinate( $key ),
$yAxis->getCoordinate( $stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] += $value )
)
)
);
}
// Force one symbol for each stacked bar
if ( !isset( $stackedSymbol[(int) ( $point->x * 10000 )] ) )
{
$stackedSymbol[(int) ( $point->x * 10000 )] = $data->symbol[$key];
}
// Store stacked value for next iteration
$side = ( $point->y == 0 ? 1 : $point->y / abs( $point->y ) );
$stacked[(int) ( $point->x * 10000 )][$side] = $point;
$renderer->drawStackedBar(
$boundings,
new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
$data->color->default,
$start,
$point,
$width,
$stackedSymbol[(int) ( $point->x * 10000 )],
$yAxisNullPosition
);
// Render highlight string if requested
if ( $data->highlight[$key] )
{
$renderer->drawDataHighlightText(
$boundings,
new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
$point,
$yAxisNullPosition,
$nr[$data->displayType->default],
$count[$data->displayType->default],
$this->options->highlightFont,
( $data->highlightValue[$key] ? $data->highlightValue[$key] : $value ),
$this->options->highlightSize + $this->options->highlightFont->padding * 2,
( $this->options->highlightLines ? $data->color[$key] : null ),
( $this->options->highlightXOffset ? $this->options->highlightXOffset : 0 ),
( $this->options->highlightYOffset ? $this->options->highlightYOffset : 0 ),
0.,
ezcGraph::LINE
);
}
break;
case $data->displayType->default === ezcGraph::BAR:
$renderer->drawBar(
$boundings,
new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
$data->color[$key],
$point,
$width,
$nr[$data->displayType->default],
$count[$data->displayType->default],
$data->symbol[$key],
$yAxisNullPosition
);
// Render highlight string if requested
if ( $data->highlight[$key] )
{
$renderer->drawDataHighlightText(
$boundings,
new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
$point,
$yAxisNullPosition,
$nr[$data->displayType->default],
$count[$data->displayType->default],
$this->options->highlightFont,
( $data->highlightValue[$key] ? $data->highlightValue[$key] : $value ),
$this->options->highlightSize + $this->options->highlightFont->padding * 2,
( $this->options->highlightLines ? $data->color[$key] : null ),
( $this->options->highlightXOffset ? $this->options->highlightXOffset : 0 ),
( $this->options->highlightYOffset ? $this->options->highlightYOffset : 0 ),
$width,
$data->displayType->default
);
}
break;
default:
throw new ezcGraphInvalidDisplayTypeException( $data->displayType->default );
break;
}
// Store last point, used to connect lines in line chart.
$lastPoint = $point;
}
}
}
/**
* Returns the default display type of the current chart type.
*
* @return int Display type
*/
public function getDefaultDisplayType()
{
return ezcGraph::LINE;
}
/**
* Check if renderer supports features requested by some special chart
* options.
*
* @throws ezcBaseValueException
* If some feature is not supported
*
* @return void
*/
protected function checkRenderer()
{
// When stacked bars are enabled, check if renderer supports them
if ( $this->options->stackBars )
{
if ( !$this->renderer instanceof ezcGraphStackedBarsRenderer )
{
throw new ezcBaseValueException( 'renderer', $this->renderer, 'ezcGraphStackedBarsRenderer' );
}
}
}
/**
* Aggregate and calculate value boundings on axis.
*
* @return void
*/
protected function setAxisValues()
{
// Virtual data set build for agrregated values sums for bar charts
$virtualBarSumDataSet = array( array(), array() );
// Calculate axis scaling and labeling
foreach ( $this->data as $dataset )
{
$nr = 0;
$labels = array();
$values = array();
foreach ( $dataset as $label => $value )
{
$labels[] = $label;
$values[] = $value;
// Build sum of all bars
if ( $this->options->stackBars &&
( $dataset->displayType->default === ezcGraph::BAR ) )
{
if ( !isset( $virtualBarSumDataSet[(int) $value >= 0][$nr] ) )
{
$virtualBarSumDataSet[(int) $value >= 0][$nr++] = $value;
}
else
{
$virtualBarSumDataSet[(int) $value >= 0][$nr++] += $value;
}
}
}
// Check if data has been associated with another custom axis, use
// default axis otherwise.
if ( $dataset->xAxis->default )
{
$dataset->xAxis->default->addData( $labels );
}
else
{
$this->elements['xAxis']->addData( $labels );
}
if ( $dataset->yAxis->default )
{
$dataset->yAxis->default->addData( $values );
}
else
{
$this->elements['yAxis']->addData( $values );
}
}
// Also use stacked bar values as base for y axis value span
// calculation
if ( $this->options->stackBars )
{
$this->elements['yAxis']->addData( $virtualBarSumDataSet[0] );
$this->elements['yAxis']->addData( $virtualBarSumDataSet[1] );
}
// There should always be something assigned to the main x and y axis.
if ( !$this->elements['xAxis']->initialized ||
!$this->elements['yAxis']->initialized )
{
throw new ezcGraphNoDataException();
}
// Calculate boundings from assigned data
$this->elements['xAxis']->calculateAxisBoundings();
$this->elements['yAxis']->calculateAxisBoundings();
}
/**
* Renders the basic elements of this chart type
*
* @param int $width
* @param int $height
* @return void
*/
protected function renderElements( $width, $height )
{
if ( !count( $this->data ) )
{
throw new ezcGraphNoDataException();
}
// Check if renderer supports requested features
$this->checkRenderer();
// Set values form datasets on axis to calculate correct spans
$this->setAxisValues();
// Generate legend
$this->elements['legend']->generateFromDataSets( $this->data );
// Get boundings from parameters
$this->options->width = $width;
$this->options->height = $height;
// Set image properties in driver
$this->driver->options->width = $width;
$this->driver->options->height = $height;
// Render subelements
$boundings = new ezcGraphBoundings();
$boundings->x1 = $this->options->width;
$boundings->y1 = $this->options->height;
$boundings = $this->elements['xAxis']->axisLabelRenderer->modifyChartBoundings(
$this->elements['yAxis']->axisLabelRenderer->modifyChartBoundings(
$boundings, new ezcGraphCoordinate( 1, 0 )
), new ezcGraphCoordinate( -1, 0 )
);
// Render subelements
foreach ( $this->elements as $name => $element )
{
// Skip element, if it should not get rendered
if ( ( $this->renderElement[$name] === false ) ||
( $name === 'xAxis' ) ||
( $name === 'yAxis' ) )
{
continue;
}
$this->driver->options->font = $element->font;
$boundings = $element->render( $this->renderer, $boundings );
}
// Set relative positions of axis in chart depending on the "null"
// value on the other axis.
$this->elements['xAxis']->nullPosition = $this->elements['yAxis']->getCoordinate( false );
$this->elements['yAxis']->nullPosition = $this->elements['xAxis']->getCoordinate( false );
// Calculate inner data boundings of chart
$innerBoundings = new ezcGraphBoundings(
$boundings->x0 + $boundings->width *
$this->elements['yAxis']->axisSpace,
$boundings->y0 + $boundings->height *
( ( $this->elements['xAxis']->outerAxisSpace === null ) ?
$this->elements['xAxis']->axisSpace :
$this->elements['xAxis']->outerAxisSpace ),
$boundings->x1 - $boundings->width *
( ( $this->elements['yAxis']->outerAxisSpace === null ) ?
$this->elements['yAxis']->axisSpace :
$this->elements['yAxis']->outerAxisSpace ),
$boundings->y1 - $boundings->height *
$this->elements['xAxis']->axisSpace
);
// Render axis
$this->driver->options->font = $this->elements['yAxis']->font;
$boundings = $this->elements['xAxis']->render( $this->renderer, $boundings, $innerBoundings );
$boundings = $this->elements['yAxis']->render( $this->renderer, $boundings, $innerBoundings );
// Render additional axis
foreach ( $this->additionalAxis as $element )
{
if ( $element->initialized )
{
// Calculate all required step sizes if values has been
// assigned to axis.
$element->calculateAxisBoundings();
}
else
{
// Do not render any axis labels, if no values were assigned
// and no step sizes were defined.
$element->axisLabelRenderer = new ezcGraphAxisNoLabelRenderer();
}
$this->driver->options->font = $element->font;
$element->nullPosition = $element->chartPosition;
$boundings = $element->render( $this->renderer, $boundings, $innerBoundings );
}
// Render graph
$this->renderData( $this->renderer, $boundings, $innerBoundings );
}
/**
* Render the line chart
*
* Renders the chart into a file or stream. The width and height are
* needed to specify the dimensions of the resulting image. For direct
* output use 'php://stdout' as output file.
*
* @param int $width Image width
* @param int $height Image height
* @param string $file Output file
* @apichange
* @return void
*/
public function render( $width, $height, $file = null )
{
$this->renderElements( $width, $height );
if ( !empty( $file ) )
{
$this->renderer->render( $file );
}
$this->renderedFile = $file;
}
/**
* Renders this chart to direct output
*
* Does the same as ezcGraphChart::render(), but renders directly to
* output and not into a file.
*
* @param int $width
* @param int $height
* @apichange
* @return void
*/
public function renderToOutput( $width, $height )
{
// @TODO: merge this function with render an deprecate ommit of third
// argument in render() when API break is possible
$this->renderElements( $width, $height );
$this->renderer->render( null );
}
}
?>