mirror of
https://github.com/seigler/neat-charts
synced 2025-07-27 01:16:09 +00:00
lots of changes, added smoothing, added many parameters, fixed grid
This commit is contained in:
parent
dd3886f46f
commit
8ace971698
3 changed files with 119 additions and 63 deletions
|
@ -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 =
|
||||
'<text x="6" y="4">'.labelFormat($yMax, $labelPlaces + 1).'</text>' .
|
||||
'<text x="6" y="'.($height + 4).'">'.labelFormat($yMin, $labelPlaces + 1).'</text>';
|
||||
'<text x="6" y="4">'.labelFormat($yMax, $labelPrecision + 1).'</text>' .
|
||||
'<text x="6" y="'.($height + 4).'">'.labelFormat($yMin, $labelPrecision + 1).'</text>';
|
||||
|
||||
// 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 .= '<text x="-4" y="'.($labelHeight + 4).'">'.labelFormat($labelY, $labelPrecision).'</text>';
|
||||
$gridLines .= " M0,".$labelHeight." ".$width.",".$labelHeight;
|
||||
}
|
||||
if (
|
||||
$labelY < $yMax - 0.3 * $labelInterval &&
|
||||
$labelY > $yMin + 0.3 * $labelInterval
|
||||
) {
|
||||
$gridText .= '<text x="-4" y="'.($labelHeight + 4).'">'.labelFormat($labelY, $labelPlaces).'</text>';
|
||||
}
|
||||
} 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 '<?xml version="1.0" standalone="no"?>
|
||||
print '<?xml version="1.0" standalone="no"?>
|
||||
<svg
|
||||
viewBox="-90 -10 '.( $width + 100 ).' '.( $height + 20 ).'"
|
||||
width="'.( $width + 100 ).'"
|
||||
|
@ -130,15 +184,15 @@ class SVGChartBuilder {
|
|||
<circle cx="1" cy="1" r="1" style="stroke: none; fill:#000000;"/>
|
||||
</marker>
|
||||
<linearGradient id="fadeFromNothing" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0.5%" stop-color="#1C75BC" stop-opacity="0"></stop>
|
||||
<stop offset="5%" stop-color="#1C75BC" stop-opacity="1"></stop>
|
||||
<stop offset="100%" stop-color="#1C75BC" stop-opacity="1"></stop>
|
||||
<stop offset="0.5%" stop-color="'.( $lineColor ).'" stop-opacity="0"></stop>
|
||||
<stop offset="5%" stop-color="'.( $lineColor ).'" stop-opacity="1"></stop>
|
||||
<stop offset="100%" stop-color="'.( $lineColor ).'" stop-opacity="1"></stop>
|
||||
</linearGradient>
|
||||
<style type="text/css"><![CDATA[
|
||||
.chart__gridLines {
|
||||
font-family: sans-serif;
|
||||
font-size: 10;
|
||||
fill: #7f7f7f;
|
||||
fill: '.( $gridLabelColor ).';
|
||||
text-anchor: end;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
@ -163,7 +217,9 @@ class SVGChartBuilder {
|
|||
<path class="chart__gridLinePaths" d="'.( $gridLines ).'" />
|
||||
'.( $gridText ).'
|
||||
</g>
|
||||
<path class="chart__plotLine" d="'.( $chartPoints ).'" />
|
||||
<g class="chart__plotLine">
|
||||
<path d="'.( $smoothed ? $chartSplines : $chartPoints ).'" />
|
||||
</g>
|
||||
</svg>';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?PHP
|
||||
<?php
|
||||
// Adapted for The Art of Web: www.the-art-of-web.com
|
||||
// Based on PHP code by Dennis Pallett: www.phpit.net
|
||||
// Please acknowledge use of this code by including this header.
|
||||
|
|
|
@ -25,4 +25,4 @@ foreach ($last24h as $item) {
|
|||
$chartData[$item->date] = $item->weightedAverage;
|
||||
}
|
||||
|
||||
print SVGChartBuilder::renderStockChart($chartData);
|
||||
print SVGChartBuilder::renderStockChart($chartData, 700, "#1C75BC");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue