diff --git a/SVGChartBuilder.php b/SVGChartBuilder.php
index d2bf19c..a9a3205 100644
--- a/SVGChartBuilder.php
+++ b/SVGChartBuilder.php
@@ -1,56 +1,109 @@
800,
+ 'height' => 250,
+ 'lineColor' => '#000',
+ 'labelColor' => '#000',
+ 'smoothed' => false,
+ 'fontSize' => 15
+ ];
- public static function renderStockChart($chartData, $options) {
- function arrayGet($array, $key, $default = NULL)
- {
- return isset($array[$key]) ? $array[$key] : $default;
- }
+ private $width;
+ private $height;
+ private $output;
+ private $xMin;
+ private $xMax;
+ private $xRange;
+ private $yMin;
+ private $yMax;
+ private $yRange;
+ private $padding = ['top'=>10, 'right'=>10, 'bottom'=>10, 'left'=>10];
- $width = arrayGet($options, "width", 800) - 100;
- $height = arrayGet($options, "height");
- $lineColor = arrayGet($options, "lineColor", "#FF2F00");
- $labelColor = arrayGet($options, "labelColor", "#999");
- $smoothed = arrayGet($options, "smoothed", false);
+ private function arrayGet($array, $key, $default = NULL)
+ {
+ return isset($array[$key]) ? $array[$key] : $default;
+ }
+
+ private function labelFormat($float, $places, $minPlaces = 0) {
+ $value = number_format($float, max($minPlaces, $places));
+ // add a trailing space if there's no decimal
+ return (strpos($value, '.') === false ? $value . '.' : $value);
+ }
+
+ /* Transform data coords to chart coords */
+ /* Transform data coords to chart coords */
+ private function transformX($x) {
+ return round(
+ ($x - $this->xMin) / $this->xRange * $this->width
+ , 2);
+ }
+ private function transformY($y) {
+ return round(
+ // SVG has y axis reversed, 0 is at the top
+ ($this->yMax - $y) / $this->yRange * $this->height
+ , 2);
+ }
+
+ private function getPrecision($value) { // thanks http://stackoverflow.com/a/21788335/5402566
+ if (!is_numeric($value)) { return false; }
+ $decimal = $value - floor($value); //get the decimal portion of the number
+ if ($decimal == 0) { return 0; } //if it's a whole number
+ $precision = strlen(trim(number_format($decimal,10),'0')) - 1; //-2 to account for '0.'
+ return $precision;
+ }
+
+ public function __construct($chartData, $options) {
+ $this->setOptions($options);
+ $this->setData($chartData);
+ }
+
+ public function setOptions($options) {
+ $this->options = array_replace($this->options, $options);
+ $this->padding['left'] = $this->options['fontSize'] * 5;
+ $this->padding['top'] = $this->padding['bottom'] = $this->options['fontSize'];
+ }
+
+ public function setData($chartData) {
// we assume $chartData is sorted by key and keys and values are all numeric
- $previousY = $previousY = null;
+ $previousX = $previousY = null;
end($chartData);
- $xMax = key($chartData);
+ $this->xMax = key($chartData);
reset($chartData);
- $xMin = key($chartData);
- $xRange = $xMax - $xMin;
+ $this->xMin = key($chartData);
+ $this->xRange = $this->xMax - $this->xMin;
$count = count($chartData);
- $deltaX = $xRange / $count;
- $yMin = INF; // so the first comparison sets this to an actual value
- $yMax = -INF;
+ $deltaX = $this->xRange / $count;
+ $this->yMin = INF; // so the first comparison sets this to an actual value
+ $this->yMax = -INF;
$averageAbsSlope = 0; // we will add all of them then divide to get an average
$secants = []; // slope between this point and the previous one
$tangents = []; // slope across the point
foreach ($chartData as $x => $y) {
- if ($y < $yMin) {
- $yMin = $y;
+ if ($y < $this->yMin) {
+ $this->yMin = $y;
$yMinX = $x;
}
- if ($y > $yMax) {
- $yMax = $y;
+ if ($y > $this->yMax) {
+ $this->yMax = $y;
$yMaxX = $x;
}
if (!is_null($previousY)) {
$averageAbsSlope += abs($y - $previousY); // just add up all the Y differences
$secants[$previousX] = ($y - $previousY) / $deltaX;
}
- if ($x == $xMax) {
+ if ($x == $this->xMax) {
$secants[$x] = ($y - $previousY) / $deltaX;
}
$previousY = $y;
$previousX = $x;
}
- $yRange = $yMax - $yMin;
- $averageAbsSlope /= $yRange * $deltaX; // turn this absolute-deltas total into a slope
+ $this->yRange = $this->yMax - $this->yMin;
+ $averageAbsSlope /= $this->yRange * $deltaX; // turn this absolute-deltas total into a slope
- if ($smoothed) {
+ if ($this->options['smoothed']) {
// take all these slopes and average them with their neighbors
// unless they change direction, then make them zero
// also restrict them a bit when they are very different
@@ -69,10 +122,10 @@ class SVGChartBuilder {
}
}
}
- if ($x == $xMax) {
+ if ($x == $this->xMax) {
$tangents[$x] = $secant;
}
- if ($x == $xMin) {
+ if ($x == $this->xMin) {
$tangents[$x] = $secant;
}
@@ -88,60 +141,41 @@ class SVGChartBuilder {
http://vis4.net/blog/posts/doing-the-line-charts-right/
*/
$aspectRatio = max(0.25, min(0.75, 1 / $averageAbsSlope));
- $height = (is_null($height) ? floor($aspectRatio * $width) : $height);
-
- function labelFormat($float, $places, $minPlaces = 0) {
- $value = number_format($float, max($minPlaces, $places));
- // add a trailing space if there's no decimal
- return (strpos($value, ".") === false ? $value . "." : $value);
+ $this->width = $this->options['width'] - $this->padding['left'] - $this->padding['right'];
+ if (isset($this->options['height'])) {
+ $this->height = $this->options['height'] - $this->padding['top'] - $this->padding['bottom'];
+ } else {
+ $this->height = floor($aspectRatio * $this->width);
+ $this->options['height'] = $this->height + $this->padding['top'] + $this->padding['bottom'];
}
- /* Transform data coords to chart coords */
- function transformX($x, $xMin, $xRange, $width) {
- return round(
- ($x - $xMin) / $xRange * $width
- , 2);
- }
- function transformY($y, $yMax, $yRange, $height) {
- return round(
- // SVG has y axis reversed, 0 is at the top
- ($yMax - $y) / $yRange * $height
- , 2);
- }
-
- function getPrecision($value) { // thanks http://stackoverflow.com/a/21788335/5402566
- if (!is_numeric($value)) { return false; }
- $decimal = $value - floor($value); //get the decimal portion of the number
- if ($decimal == 0) { return 0; } //if it's a whole number
- $precision = strlen(trim(number_format($decimal,10),"0")) - 1; //-2 to account for "0."
- return $precision;
- }
-
- $chartPoints = "M";
- $chartSplines = "M".
- transformX($xMin, $xMin, $xRange, $width).",".
- transformY($chartData[$xMin], $yMax, $yRange, $height);
- foreach ($chartData as $x => $y) {
- $chartPoints .=
- transformX($x, $xMin, $xRange, $width).",".
- transformY($y, $yMax, $yRange, $height) . "\n";
-
- if ($smoothed) {
+ $chartPoints = 'M';
+ $chartSplines = 'M'.
+ $this->transformX($this->xMin).','.
+ $this->transformY($chartData[$this->xMin]);
+ if ($this->options['smoothed']) {
+ foreach ($chartData as $x => $y) {
$controlX = $deltaX / 3 / sqrt(1 + $tangents[$x]**2);
$controlY = $tangents[$x] * $controlX;
- if ($x != $xMin) {
- $chartSplines .= " S".
- transformX($x - $controlX, $xMin, $xRange, $width).",".
- transformY($y - $controlY, $yMax, $yRange, $height)." ".
- transformX($x, $xMin, $xRange, $width).",".
- transformY($y, $yMax, $yRange, $height);
+ if ($x != $this->xMin) {
+ $chartSplines .= ' S'.
+ $this->transformX($x - $controlX).','.
+ $this->transformY($y - $controlY).' '.
+ $this->transformX($x).','.
+ $this->transformY($y);
}
}
+ } else {
+ foreach ($chartData as $x => $y) {
+ $chartPoints .=
+ $this->transformX($x).','.
+ $this->transformY($y) . ' ';
+ }
}
- $numLabels = 2 + ceil($height / 60);
- $labelInterval = $yRange / $numLabels;
- $labelModulation = 10 ** (1 + floor(-log($yRange / $numLabels, 10)));
+ $numLabels = 2 + ceil($this->height / $this->options['fontSize'] / 6);
+ $labelInterval = $this->yRange / $numLabels;
+ $labelModulation = 10 ** (1 + floor(-log($this->yRange / $numLabels, 10)));
// 0.1 here is a fudge factor so we get multiples of 2.5 a little more often
if (fmod($labelInterval * $labelModulation, 2.5) < fmod($labelInterval * $labelModulation, 2) + 0.1) {
@@ -150,90 +184,93 @@ class SVGChartBuilder {
$labelModulation /= 2;
}
$labelInterval = ceil($labelInterval * $labelModulation) / $labelModulation;
- $labelPrecision = getPrecision($labelInterval);
+ $labelPrecision = $this->getPrecision($labelInterval);
// Top and bottom grid lines
$gridLines =
- "M10,0 ".$width.",0\n".
- "M10,".$height.",".$width.",".$height."\n";
+ 'M10,0 '.$this->width.',0 '.
+ ' M10,'.$this->height.','.$this->width.','.$this->height;
// Top and bottom grid labels
$gridText =
- ''.labelFormat($yMax, $labelPrecision + 1).'' .
- ''.labelFormat($yMin, $labelPrecision + 1).'';
+ ''.($this->labelFormat($this->yMax, $labelPrecision + 1)).'' .
+ ''.($this->labelFormat($this->yMin, $labelPrecision + 1)).'';
// Main labels and grid lines
for (
- $labelY = $yMin - fmod($yMin, $labelInterval) + $labelInterval; // Start at the first "nice" Y value > min
- $labelY < $yMax; // Keep going until max
+ $labelY = $this->yMin - fmod($this->yMin, $labelInterval) + $labelInterval; // Start at the first "nice" Y value > min
+ $labelY < $this->yMax; // Keep going until max
$labelY += $labelInterval // Add Interval each iteration
) {
- $labelHeight = transformY($labelY, $yMax, $yRange, $height);
+ $labelHeight = $this->transformY($labelY);
if ( // label is not too close to the min or max
- $labelHeight < $height - 25 &&
- $labelHeight > 25
+ $labelHeight < $this->height - 2.5 * $this->options['fontSize'] &&
+ $labelHeight > $this->options['fontSize'] * 2.5
) {
- $gridText .= ''.labelFormat($labelY, $labelPrecision).'';
- $gridLines .= " M0,".$labelHeight." ".$width.",".$labelHeight;
+ $gridText .= ''.$this->labelFormat($labelY, $labelPrecision).'';
+ $gridLines .= ' M0,'.$labelHeight.' '.$this->width.','.$labelHeight;
} else if ( // label is too close
- $labelHeight < $height - 4 &&
- $labelHeight > 4
+ $labelHeight < $this->height - $this->options['fontSize'] / 2 &&
+ $labelHeight > $this->options['fontSize'] / 2
) {
- $gridLines .= " M".( // move grid line over when it's very close to the min or max label
- $labelHeight < $height - 10 && $labelHeight > 10 ? 0 : 10
- ).",".$labelHeight." ".$width.",".$labelHeight;
+ $gridLines .= ' M'.( // move grid line over when it's very close to the min or max label
+ $labelHeight < $this->height - $this->options['fontSize'] / 2 && $labelHeight > $this->options['fontSize'] / 2 ? 0 : $this->options['fontSize'] / 2
+ ).','.$labelHeight.' '.$this->width.','.$labelHeight;
}
}
- print '
-