diff --git a/SVGChartBuilder.php b/SVGChartBuilder.php
index 4d5c803..86092d3 100644
--- a/SVGChartBuilder.php
+++ b/SVGChartBuilder.php
@@ -2,34 +2,72 @@
class SVGChartBuilder {
- public static function renderStockChart($chartData, $width = 700) {
- $absoluteDeltas = [];
- $previousValue = null;
- $yMin = $xMin = INF;
- $yMax = $xMax = -INF;
-
- foreach ($chartData as $thisX => $thisY) {
- if (!is_null($previousValue)) {
- $absoluteDeltas[] = abs($thisY - $previousValue);
- }
- if ($thisY < $yMin) {
- $yMin = $thisY;
- $yMinX = $thisX;
- }
- if ($thisY > $yMax) {
- $yMax = $thisY;
- $yMaxX = $thisX;
- }
- $previousValue = $thisY;
- }
- $yRange = $yMax - $yMin;
+ public static function renderStockChart($chartData, $width = 700, $lineColor = "#FF2F00", $gridLabelColor = "#999", $smoothed = false) {
+ // we assume $chartData is sorted by key and keys and values are all numeric
+ $previousY = $previousY = null;
end($chartData);
$xMax = key($chartData);
reset($chartData);
$xMin = key($chartData);
$xRange = $xMax - $xMin;
$count = count($chartData);
- $averageDelta = abs(array_sum($absoluteDeltas)/$count);
+ $deltaX = $xRange / $count;
+ $yMin = INF; // so the first comparison sets this to an actual value
+ $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;
+ $yMinX = $x;
+ }
+ if ($y > $yMax) {
+ $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) {
+ $secants[$x] = ($y - $previousY) / $deltaX;
+ }
+ $previousY = $y;
+ $previousX = $x;
+ }
+ $yRange = $yMax - $yMin;
+ $averageAbsSlope /= $yRange * $deltaX; // turn this absolute-deltas total into a slope
+
+ // 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
+ $previousSecant = $previousX = null;
+ foreach ($secants as $x => $secant) {
+ if (!is_null($previousSecant)) {
+ $tangents[$x] = ($secant + $previousSecant) / 2;
+ if ($secant == 0 || $previousSecant == 0 || $secant * $previousSecant <= 0)
+ {
+ $tangents[$x] = 0;
+ } else {
+ if ($tangents[$x] / $previousSecant > 3) {
+ $tangents[$x] = 3 * $previousSecant;
+ } else if ($tangents[$x] / $secant > 3) {
+ $tangents[$x] = 3 * $secant;
+ }
+ }
+ }
+ if ($x == $xMax) {
+ $tangents[$x] = $secant;
+ }
+ if ($x == $xMin) {
+ $tangents[$x] = $secant;
+ }
+
+ $previousX = $x;
+ $previousSecant = $secant;
+ }
/*
We want the height of the median y-delta to be the same as
@@ -37,11 +75,13 @@ class SVGChartBuilder {
45 degrees. This improves comprehension.
http://vis4.net/blog/posts/doing-the-line-charts-right/
*/
- $aspectRatio = max(0.25, $yRange / $averageDelta / $count);
+ $aspectRatio = max(0.25, min(0.75, 1 / $averageAbsSlope));
$height = floor($aspectRatio * $width);
- function labelFormat($float, $sigFigs, $minPlaces = 0) {
- return number_format($float, max($minPlaces, $sigFigs));
+ 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 */
@@ -66,58 +106,72 @@ class SVGChartBuilder {
}
$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) . '
- ';
+ $chartPoints .=
+ transformX($x, $xMin, $xRange, $width).",".
+ transformY($y, $yMax, $yRange, $height) . "\n";
+
+ $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);
+ }
}
- $numLabels = min(4, ceil($height / 40));
+ $numLabels = 1 + ceil($height / 60);
$labelInterval = $yRange / $numLabels;
$labelModulation = 10 ** (1 + floor(-log($yRange / $numLabels, 10)));
-// if (fmod($labelInterval, $labelModulation / 5) < $labelInterval * 0.5) {
+
+ // 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) {
$labelModulation /= 2.5;
-// } else if (fmod($labelInterval, $labelModulation / 2) < $labelInterval * 0.25) {
-// $labelModulation /= 2;
-// }
-// var_dump($labelInterval, $labelModulation, $labelInterval * $labelModulation, ceil($labelInterval * $labelModulation) / $labelModulation);
+ } else {
+ $labelModulation /= 2;
+ }
$labelInterval = ceil($labelInterval * $labelModulation) / $labelModulation;
- $labelPlaces = getPrecision($labelInterval);
+ $labelPrecision = getPrecision($labelInterval);
// Top and bottom grid lines
$gridLines =
- "M10,0 ".$width.",0
- M10,".$height.",".$width.",".$height."
- ";
+ "M10,0 ".$width.",0\n".
+ "M10,".$height.",".$width.",".$height."\n";
// Top and bottom grid labels
$gridText =
- ''.labelFormat($yMax, $labelPlaces + 1).'' .
- ''.labelFormat($yMin, $labelPlaces + 1).'';
+ ''.labelFormat($yMax, $labelPrecision + 1).'' .
+ ''.labelFormat($yMin, $labelPrecision + 1).'';
- // Start at the first "nice" Y value > min + 50% of the interval
- // Keep going until max - 50% of the interval
- // Add Interval each iteration
+ // Main labels and grid lines
for (
- $labelY = $yMin - fmod($yMin, $labelInterval) + $labelInterval;
- $labelY < $yMax;
- $labelY += $labelInterval
+ $labelY = $yMin - fmod($yMin, $labelInterval) + $labelInterval; // Start at the first "nice" Y value > min
+ $labelY < $yMax; // Keep going until max
+ $labelY += $labelInterval // Add Interval each iteration
) {
$labelHeight = transformY($labelY, $yMax, $yRange, $height);
- if (
- $labelY < $yMax - 0.1 * $labelInterval &&
- $labelY > $yMin + 0.1 * $labelInterval
+ if ( // label is not too close to the min or max
+ $labelHeight < $height - 25 &&
+ $labelHeight > 25
) {
+ $gridText .= ''.labelFormat($labelY, $labelPrecision).'';
$gridLines .= " M0,".$labelHeight." ".$width.",".$labelHeight;
- }
- if (
- $labelY < $yMax - 0.3 * $labelInterval &&
- $labelY > $yMin + 0.3 * $labelInterval
- ) {
- $gridText .= ''.labelFormat($labelY, $labelPlaces).'';
- }
+ } else if ( // label is too close
+ $labelHeight < $height - 4 &&
+ $labelHeight > 4
+ ) {
+ $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;
+ }
}
- return '
+ print '
-
-
-
+
+
+