| <?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 ); |
| } |
| } |
| ?> |