$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
if ($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
$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
the width of one x-delta, which puts the median slope at
45 degrees. This improves comprehension.
http://vis4.net/blog/posts/doing-the-line-charts-right/
*/
$aspectRatio = max(0.25, min(0.75, 1 / $averageAbsSlope));
$height = $height ?? floor($aspectRatio * $width);
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 */
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) {
$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 = 2 + ceil($height / 60);
$labelInterval = $yRange / $numLabels;
$labelModulation = 10 ** (1 + floor(-log($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) {
$labelModulation /= 2.5;
} else {
$labelModulation /= 2;
}
$labelInterval = ceil($labelInterval * $labelModulation) / $labelModulation;
$labelPrecision = getPrecision($labelInterval);
// Top and bottom grid lines
$gridLines =
"M10,0 ".$width.",0\n".
"M10,".$height.",".$width.",".$height."\n";
// Top and bottom grid labels
$gridText =
''.labelFormat($yMax, $labelPrecision + 1).'' .
''.labelFormat($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 += $labelInterval // Add Interval each iteration
) {
$labelHeight = transformY($labelY, $yMax, $yRange, $height);
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;
} 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;
}
}
print '
';
}
}