blob: 6f431d07b89d508e37ff33003a8b6b8fcec11b3c [file] [log] [blame]
<?php
/**
* File containing the ezcGraphChartElementDateAxis 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//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Class to represent date axis.
*
* Axis elements represent the axis in a bar, line or radar chart. They are
* chart elements (ezcGraphChartElement) extending from
* ezcGraphChartElementAxis, where additional formatting options can be found.
* You should generally use the axis, which matches your input data best, so
* that the automatic chart layouting works best. Aavailable axis types are:
*
* - ezcGraphChartElementDateAxis
* - ezcGraphChartElementLabeledAxis
* - ezcGraphChartElementLogarithmicalAxis
* - ezcGraphChartElementNumericAxis
*
* Date axis will try to find a "nice" interval based on the values on the x
* axis. If non numeric values are given, ezcGraphChartElementDateAxis will
* convert them to timestamps using PHPs strtotime function.
*
* It is always possible to set start date, end date and the interval manually
* by yourself.
*
* The $dateFormat option provides an additional way of formatting the labels
* used on the axis. The options from the parent class $formatString and
* $labelCallback do still apply.
*
* You may use a date axis like in the following example:
*
* <code>
* $graph = new ezcGraphLineChart();
* $graph->options->fillLines = 210;
* $graph->title = 'Concurrent requests';
* $graph->legend = false;
*
* $graph->xAxis = new ezcGraphChartElementDateAxis();
*
* // Add data
* $graph->data['Machine 1'] = new ezcGraphArrayDataSet( array(
* '8:00' => 3241,
* '8:13' => 934,
* '8:24' => 1201,
* '8:27' => 1752,
* '8:51' => 123,
* ) );
* $graph->data['Machine 2'] = new ezcGraphArrayDataSet( array(
* '8:05' => 623,
* '8:12' => 2103,
* '8:33' => 543,
* '8:43' => 2034,
* '8:59' => 3410,
* ) );
*
* $graph->data['Machine 1']->symbol = ezcGraph::BULLET;
* $graph->data['Machine 2']->symbol = ezcGraph::BULLET;
*
* $graph->render( 400, 150, 'tutorial_axis_datetime.svg' );
* </code>
*
* @property float $startDate
* Starting date used to display on axis.
* @property float $endDate
* End date used to display on axis.
* @property float $interval
* Time interval between steps on axis.
* @property string $dateFormat
* Format of date string
* Like http://php.net/date
*
* @version //autogentag//
* @package Graph
* @mainclass
*/
class ezcGraphChartElementDateAxis extends ezcGraphChartElementAxis
{
const MONTH = 2629800;
const YEAR = 31536000;
const DECADE = 315360000;
/**
* Minimum inserted date
*
* @var int
*/
protected $minValue = false;
/**
* Maximum inserted date
*
* @var int
*/
protected $maxValue = false;
/**
* Nice time intervals to used if there is no user defined interval
*
* @var array
*/
protected $predefinedIntervals = array(
// Second
1 => 'H:i.s',
// Ten seconds
10 => 'H:i.s',
// Thirty seconds
30 => 'H:i.s',
// Minute
60 => 'H:i',
// Ten minutes
600 => 'H:i',
// Half an hour
1800 => 'H:i',
// Hour
3600 => 'H:i',
// Four hours
14400 => 'H:i',
// Six hours
21600 => 'H:i',
// Half a day
43200 => 'd.m a',
// Day
86400 => 'd.m',
// Week
604800 => 'W',
// Month
self::MONTH => 'M y',
// Year
self::YEAR => 'Y',
// Decade
self::DECADE => 'Y',
);
/**
* Constant used for calculation of automatic definition of major scaling
* steps
*/
const MAJOR_COUNT = 10;
/**
* Constructor
*
* @param array $options Default option array
* @return void
* @ignore
*/
public function __construct( array $options = array() )
{
$this->properties['startDate'] = false;
$this->properties['endDate'] = false;
$this->properties['interval'] = false;
$this->properties['dateFormat'] = false;
parent::__construct( $options );
}
/**
* __set
*
* @param mixed $propertyName
* @param mixed $propertyValue
* @throws ezcBaseValueException
* If a submitted parameter was out of range or type.
* @throws ezcBasePropertyNotFoundException
* If a the value for the property options is not an instance of
* @return void
* @ignore
*/
public function __set( $propertyName, $propertyValue )
{
switch ( $propertyName )
{
case 'startDate':
$this->properties['startDate'] = (int) $propertyValue;
break;
case 'endDate':
$this->properties['endDate'] = (int) $propertyValue;
break;
case 'interval':
$this->properties['interval'] = (int) $propertyValue;
$this->properties['initialized'] = true;
break;
case 'dateFormat':
$this->properties['dateFormat'] = (string) $propertyValue;
break;
default:
parent::__set( $propertyName, $propertyValue );
break;
}
}
/**
* Ensure proper timestamp
*
* Takes a mixed value from datasets, like timestamps, or strings
* describing some time and converts it to a timestamp.
*
* @param mixed $value
* @return int
*/
protected static function ensureTimestamp( $value )
{
if ( is_numeric( $value ) )
{
$timestamp = (int) $value;
}
elseif ( ( $timestamp = strtotime( $value ) ) === false )
{
throw new ezcGraphErrorParsingDateException( $value );
}
return $timestamp;
}
/**
* Add data for this axis
*
* @param array $values Value which will be displayed on this axis
* @return void
*/
public function addData( array $values )
{
foreach ( $values as $nr => $value )
{
$value = self::ensureTimestamp( $value );
if ( $this->minValue === false ||
$value < $this->minValue )
{
$this->minValue = $value;
}
if ( $this->maxValue === false ||
$value > $this->maxValue )
{
$this->maxValue = $value;
}
}
$this->properties['initialized'] = true;
}
/**
* Calculate nice time interval
*
* Use the best fitting time interval defined in class property array
* predefinedIntervals.
*
* @param int $min Start time
* @param int $max End time
* @return void
*/
protected function calculateInterval( $min, $max )
{
$diff = $max - $min;
foreach ( $this->predefinedIntervals as $interval => $format )
{
if ( ( $diff / $interval ) <= self::MAJOR_COUNT )
{
break;
}
}
if ( ( $this->properties['startDate'] !== false ) &&
( $this->properties['endDate'] !== false ) )
{
// Use interval between defined borders
if ( ( $diff % $interval ) > 0 )
{
// Stil use predefined date format from old interval if not set
if ( $this->properties['dateFormat'] === false )
{
$this->properties['dateFormat'] = $this->predefinedIntervals[$interval];
}
$count = ceil( $diff / $interval );
$interval = round( $diff / $count, 0 );
}
}
$this->properties['interval'] = $interval;
}
/**
* Calculate lower nice date
*
* Calculates a date which is earlier or equal to the given date, and is
* divisible by the given interval.
*
* @param int $min Date
* @param int $interval Interval
* @return int Earlier date
*/
protected function calculateLowerNiceDate( $min, $interval )
{
switch ( $interval )
{
case self::MONTH:
// Special handling for months - not covered by the default
// algorithm
return mktime(
1,
0,
0,
(int) date( 'm', $min ),
1,
(int) date( 'Y', $min )
);
default:
$dateSteps = array( 60, 60, 24, 7, 52 );
$date = array(
(int) date( 's', $min ),
(int) date( 'i', $min ),
(int) date( 'H', $min ),
(int) date( 'd', $min ),
(int) date( 'm', $min ),
(int) date( 'Y', $min ),
);
$element = 0;
while ( ( $step = array_shift( $dateSteps ) ) &&
( $interval > $step ) )
{
$interval /= $step;
$date[$element++] = (int) ( $element > 2 );
}
$date[$element] -= $date[$element] % $interval;
return mktime(
$date[2],
$date[1],
$date[0],
$date[4],
$date[3],
$date[5]
);
}
}
/**
* Calculate start date
*
* Use calculateLowerNiceDate to get a date earlier or equal date then the
* minimum date to use it as the start date for the axis depending on the
* selected interval.
*
* @param mixed $min Minimum date
* @param mixed $max Maximum date
* @return void
*/
public function calculateMinimum( $min, $max )
{
if ( $this->properties['endDate'] === false )
{
$this->properties['startDate'] = $this->calculateLowerNiceDate( $min, $this->interval );
}
else
{
$this->properties['startDate'] = $this->properties['endDate'];
while ( $this->properties['startDate'] > $min )
{
switch ( $this->interval )
{
case self::MONTH:
$this->properties['startDate'] = strtotime( '-1 month', $this->properties['startDate'] );
break;
case self::YEAR:
$this->properties['startDate'] = strtotime( '-1 year', $this->properties['startDate'] );
break;
case self::DECADE:
$this->properties['startDate'] = strtotime( '-10 years', $this->properties['startDate'] );
break;
default:
$this->properties['startDate'] -= $this->interval;
}
}
}
}
/**
* Calculate end date
*
* Use calculateLowerNiceDate to get a date later or equal date then the
* maximum date to use it as the end date for the axis depending on the
* selected interval.
*
* @param mixed $min Minimum date
* @param mixed $max Maximum date
* @return void
*/
public function calculateMaximum( $min, $max )
{
$this->properties['endDate'] = $this->properties['startDate'];
while ( $this->properties['endDate'] < $max )
{
switch ( $this->interval )
{
case self::MONTH:
$this->properties['endDate'] = strtotime( '+1 month', $this->properties['endDate'] );
break;
case self::YEAR:
$this->properties['endDate'] = strtotime( '+1 year', $this->properties['endDate'] );
break;
case self::DECADE:
$this->properties['endDate'] = strtotime( '+10 years', $this->properties['endDate'] );
break;
default:
$this->properties['endDate'] += $this->interval;
}
}
}
/**
* Calculate axis bounding values on base of the assigned values
*
* @return void
*/
public function calculateAxisBoundings()
{
// Prevent division by zero, when min == max
if ( $this->minValue == $this->maxValue )
{
if ( $this->minValue == 0 )
{
$this->maxValue = 1;
}
else
{
$this->minValue -= ( $this->minValue * .1 );
$this->maxValue += ( $this->maxValue * .1 );
}
}
// Use custom minimum and maximum if available
if ( $this->properties['startDate'] !== false )
{
$this->minValue = $this->properties['startDate'];
}
if ( $this->properties['endDate'] !== false )
{
$this->maxValue = $this->properties['endDate'];
}
// Calculate "nice" values for scaling parameters
if ( $this->properties['interval'] === false )
{
$this->calculateInterval( $this->minValue, $this->maxValue );
}
if ( $this->properties['dateFormat'] === false && isset( $this->predefinedIntervals[$this->interval] ) )
{
$this->properties['dateFormat'] = $this->predefinedIntervals[$this->interval];
}
if ( $this->properties['startDate'] === false )
{
$this->calculateMinimum( $this->minValue, $this->maxValue );
}
if ( $this->properties['endDate'] === false )
{
$this->calculateMaximum( $this->minValue, $this->maxValue );
}
}
/**
* Get coordinate for a dedicated value on the chart
*
* @param float $value Value to determine position for
* @return float Position on chart
*/
public function getCoordinate( $value )
{
// Force typecast, because ( false < -100 ) results in (bool) true
$intValue = ( $value === false ? false : self::ensureTimestamp( $value ) );
if ( ( $value === false ) &&
( ( $intValue < $this->startDate ) || ( $intValue > $this->endDate ) ) )
{
switch ( $this->position )
{
case ezcGraph::LEFT:
case ezcGraph::TOP:
return 0.;
case ezcGraph::RIGHT:
case ezcGraph::BOTTOM:
return 1.;
}
}
else
{
switch ( $this->position )
{
case ezcGraph::LEFT:
case ezcGraph::TOP:
return ( $intValue - $this->startDate ) / ( $this->endDate - $this->startDate );
case ezcGraph::RIGHT:
case ezcGraph::BOTTOM:
return 1 - ( $intValue - $this->startDate ) / ( $this->endDate - $this->startDate );
}
}
}
/**
* Return count of minor steps
*
* @return integer Count of minor steps
*/
public function getMinorStepCount()
{
return false;
}
/**
* Return count of major steps
*
* @return integer Count of major steps
*/
public function getMajorStepCount()
{
return (int) ceil( ( $this->properties['endDate'] - $this->startDate ) / $this->interval );
}
/**
* Get label for a dedicated step on the axis
*
* @param integer $step Number of step
* @return string label
*/
public function getLabel( $step )
{
return $this->getLabelFromTimestamp( $this->startDate + ( $step * $this->interval ), $step );
}
/**
* Get label for timestamp
*
* @param int $time
* @param int $step
* @return string
*/
protected function getLabelFromTimestamp( $time, $step )
{
if ( $this->properties['labelCallback'] !== null )
{
return call_user_func_array(
$this->properties['labelCallback'],
array(
date( $this->properties['dateFormat'], $time ),
$step,
)
);
}
else
{
return date( $this->properties['dateFormat'], $time );
}
}
/**
* Return array of steps on this axis
*
* @return array( ezcGraphAxisStep )
*/
public function getSteps()
{
$steps = array();
$start = $this->properties['startDate'];
$end = $this->properties['endDate'];
$distance = $end - $start;
$step = 0;
for ( $time = $start; $time <= $end; )
{
$steps[] = new ezcGraphAxisStep(
( $time - $start ) / $distance,
$this->interval / $distance,
$this->getLabelFromTimestamp( $time, $step++ ),
array(),
$step === 1,
$time >= $end
);
switch ( $this->interval )
{
case self::MONTH:
$time = strtotime( '+1 month', $time );
break;
case self::YEAR:
$time = strtotime( '+1 year', $time );
break;
case self::DECADE:
$time = strtotime( '+10 years', $time );
break;
default:
$time += $this->interval;
break;
}
}
return $steps;
}
/**
* Is zero step
*
* Returns true if the given step is the one on the initial axis position
*
* @param int $step Number of step
* @return bool Status If given step is initial axis position
*/
public function isZeroStep( $step )
{
return ( $step == 0 );
}
}
?>