improved demo, added "honest charts" zero-axis option

This commit is contained in:
Joshua Seigler 2016-07-04 03:10:54 -04:00
parent a582c2f97d
commit bbe218d7fe
3 changed files with 70 additions and 48 deletions

View file

@ -3,9 +3,9 @@ require_once 'vendor/autoload.php';
function randomData($count = 96) { function randomData($count = 96) {
$randomData = []; $randomData = [];
$offset = 100 * (rand()/getRandMax())**4; $offset = 100 * (rand()/getRandMax())**2;
$scale = 100 * (rand()/getRandMax())**2; $scale = max(0.1 * $offset, 100 * rand() / getRandMax());
$volatility = 0.5 * (rand()/getRandMax())**3; $volatility = 0.25 * (rand()/getRandMax())**3 + 0.25;
for ($n = 0, $current = $offset + 0.5 * $scale; $n < $count; $n++) { for ($n = 0, $current = $offset + 0.5 * $scale; $n < $count; $n++) {
$current -= $offset; $current -= $offset;
$current *= 1 + $volatility * (rand()/getRandMax() - 0.5); $current *= 1 + $volatility * (rand()/getRandMax() - 0.5);
@ -32,14 +32,14 @@ function randomData($count = 96) {
</header> </header>
<main> <main>
<section> <section>
<h2>SVG chart in <code>img</code> tag</h2> <h2>Chart in <code>img</code> tag</h2>
<figure> <figure>
<img src="./demo-as-image.php"> <img src="./demo-as-image.php">
<figcaption>Random generated data, loaded as an image</figcaption> <figcaption>Random generated data, loaded as an image</figcaption>
</figure> </figure>
</section> </section>
<section> <section>
<h2>SVG chart in <code>svg</code> tag</h2> <h2>Chart in <code>svg</code> tag, zero axis shown</h2>
<figure> <figure>
<?php <?php
$chart = new NeatCharts\LineChart(randomData(), [ $chart = new NeatCharts\LineChart(randomData(), [
@ -48,7 +48,8 @@ $chart = new NeatCharts\LineChart(randomData(), [
'lineColor'=>'#F00', 'lineColor'=>'#F00',
'labelColor'=>'#222', 'labelColor'=>'#222',
'smoothed'=>false, 'smoothed'=>false,
'fontSize'=>14 'fontSize'=>14,
'yAxisZero'=>true
]); ]);
echo $chart->render(); echo $chart->render();
?> ?>
@ -56,7 +57,7 @@ echo $chart->render();
</figure> </figure>
</section> </section>
<section> <section>
<h2>Smoothed SVG chart in <code>svg</code> tag</h2> <h2>Smoothed chart in <code>svg</code> tag</h2>
<figure> <figure>
<?php <?php
$chart = new NeatCharts\LineChart(randomData(12), [ $chart = new NeatCharts\LineChart(randomData(12), [
@ -73,7 +74,7 @@ echo $chart->render();
</figure> </figure>
</section> </section>
<section> <section>
<h2>SVG sparkline in <code>svg</code> tag</h2> <h2>Sparkline in <code>svg</code> tag</h2>
<figure> <figure>
<?php <?php
$chart = new NeatCharts\LineChart(randomData(48), [ $chart = new NeatCharts\LineChart(randomData(48), [

View file

@ -1,31 +1,33 @@
<?php <?php
namespace NeatCharts { namespace NeatCharts {
class LineChart extends NeatChart { class LineChart extends NeatChart {
public function setOptions($options) {
$this->options = [ // LineChart defaults
'width' => 800,
'height' => 250,
'lineColor' => '#000',
'markerColor' => '#000',
'labelColor' => '#000',
'smoothed' => false,
'fontSize' => 15,
'yAxisEnabled'=>true,
'xAxisEnabled'=>false,
'yAxisZero'=>false
];
parent::setOptions($options);
}
public function setData($chartData) { public function setData($chartData) {
$this->setWindow($chartData, $this->options); // sets min, max, range, etc
// we assume $chartData is sorted by key and keys and values are all numeric // we assume $chartData is sorted by key and keys and values are all numeric
$previousX = $previousY = null; $previousX = $previousY = null;
end($chartData);
$this->xMax = key($chartData);
reset($chartData);
$this->xMin = key($chartData);
$this->xRange = $this->xMax - $this->xMin;
$count = count($chartData); $count = count($chartData);
$deltaX = $this->xRange / $count; $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 $averageAbsSlope = 0; // we will add all of them then divide to get an average
$secants = []; // slope between this point and the previous one $secants = []; // slope between this point and the previous one
$tangents = []; // slope across the point $tangents = []; // slope across the point
foreach ($chartData as $x => $y) { foreach ($chartData as $x => $y) {
if ($y < $this->yMin) {
$this->yMin = $y;
$yMinX = $x;
}
if ($y > $this->yMax) {
$this->yMax = $y;
$yMaxX = $x;
}
if (!is_null($previousY)) { if (!is_null($previousY)) {
$averageAbsSlope += abs($y - $previousY); // just add up all the Y differences $averageAbsSlope += abs($y - $previousY); // just add up all the Y differences
$secants[$previousX] = ($y - $previousY) / $deltaX; $secants[$previousX] = ($y - $previousY) / $deltaX;
@ -36,7 +38,6 @@ namespace NeatCharts {
$previousY = $y; $previousY = $y;
$previousX = $x; $previousX = $x;
} }
$this->yRange = $this->yMax - $this->yMin;
$averageAbsSlope /= $this->yRange * $deltaX; // turn this absolute-deltas total into a slope $averageAbsSlope /= $this->yRange * $deltaX; // turn this absolute-deltas total into a slope
if ($this->options['smoothed']) { if ($this->options['smoothed']) {
@ -140,15 +141,13 @@ namespace NeatCharts {
} }
$chartPoints = 'M'; $chartPoints = 'M';
$chartSplines = 'M'.
$this->transformX($this->xMin).','.
$this->transformY($chartData[$this->xMin]);
if ($this->options['smoothed']) { if ($this->options['smoothed']) {
$chartPoints .= $this->transformX($this->xMin).','.$this->transformY($chartData[$this->xMin]);
foreach ($chartData as $x => $y) { foreach ($chartData as $x => $y) {
$controlX = $deltaX / 3 / sqrt(1 + $tangents[$x]**2); $controlX = $deltaX / 3 / sqrt(1 + $tangents[$x]**2);
$controlY = $tangents[$x] * $controlX; $controlY = $tangents[$x] * $controlX;
if ($x != $this->xMin) { if ($x != $this->xMin) {
$chartSplines .= ' S'. $chartPoints .= ' S'.
$this->transformX($x - $controlX).','. $this->transformX($x - $controlX).','.
$this->transformY($y - $controlY).' '. $this->transformY($y - $controlY).' '.
$this->transformX($x).','. $this->transformX($x).','.
@ -166,31 +165,29 @@ namespace NeatCharts {
$chartID = rand(); $chartID = rand();
$this->output = '<svg viewBox="-'.( $this->padding['left'] ).' -'.( $this->padding['top'] ).' '.( $this->options['width'] ).' '.( $this->options['height'] ).'" width="'.( $this->options['width'] ).'" height="'.( $this->options['height'] ).'" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> $this->output = '<svg viewBox="-'.( $this->padding['left'] ).' -'.( $this->padding['top'] ).' '.( $this->options['width'] ).' '.( $this->options['height'] ).'" width="'.( $this->options['width'] ).'" height="'.( $this->options['height'] ).'" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs> <defs>
<marker id="SVGChart-markerCircle-'.( $chartID ).'" markerWidth="2" markerHeight="2" refX="1" refY="1" markerUnits="strokeWidth"> <marker id="neatchart-markerCircle-'.( $chartID ).'" markerWidth="2" markerHeight="2" refX="1" refY="1" markerUnits="strokeWidth">
<circle cx="1" cy="1" r="1" style="stroke: none; fill:'.( $this->options['markerColor'] ).';" /> <circle class="neatchart-marker" cx="1" cy="1" r="1" stroke="none" fill="'.( $this->options['markerColor'] ).'" />
</marker> </marker>
<linearGradient id="SVGChart-fadeFromNothing-'.( $chartID ).'" x1="0%" y1="0%" x2="100%" y2="0%"> <linearGradient id="neatchart-fadeFromNothing-'.( $chartID ).'" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0.5%" stop-color="'.( $this->options['lineColor'] ).'" stop-opacity="0"></stop> <stop offset="0.5%" stop-color="'.( $this->options['lineColor'] ).'" stop-opacity="0"></stop>
<stop offset="5%" stop-color="'.( $this->options['lineColor'] ).'" stop-opacity="1"></stop> <stop offset="2%" stop-color="'.( $this->options['lineColor'] ).'" stop-opacity="1"></stop>
<stop offset="100%" stop-color="'.( $this->options['lineColor'] ).'" stop-opacity="1"></stop> <stop offset="100%" stop-color="'.( $this->options['lineColor'] ).'" stop-opacity="1"></stop>
</linearGradient> </linearGradient>
</defs> </defs>
<g class="SVGChart">'.( $this->options['yAxisEnabled'] || $this->options['xAxisEnabled'] ? ' <g class="neatchart">'.( $this->options['yAxisEnabled'] || $this->options['xAxisEnabled'] ? '
<g class="chart__gridLines" <g class="chart__gridLines"
shape-rendering="crispEdges"
fill="none" fill="none"
stroke="'.( $this->options['labelColor'] ).'" stroke="'.( $this->options['labelColor'] ).'"
stroke-opacity="0.75"
stroke-width="1" stroke-width="1"
vector-effect="non-scaling-stroke"
stroke-dasharray="2, 2" stroke-dasharray="2, 2"
> shape-rendering="crispEdges">
<path class="chart__gridLinePaths" d="'.( $gridLines ).'" /> <path class="chart__gridLinePaths" d="'.( $gridLines ).'" />
</g> </g>
<g class="chart__gridLabels" <g class="chart__gridLabels"
fill="'.( $this->options['labelColor'] ).'" fill="'.( $this->options['labelColor'] ).'"
font-family="monospace" font-family="monospace"
font-size="'.( $this->options['fontSize'] ).'px" font-size="'.( $this->options['fontSize'] ).'px">
>
'.( $gridText ).' '.( $gridText ).'
</g>' : '').' </g>' : '').'
<g class="chart__plotLine" <g class="chart__plotLine"
@ -198,10 +195,16 @@ namespace NeatCharts {
stroke-width="'.( $this->options['fontSize'] / 3 ).'" stroke-width="'.( $this->options['fontSize'] / 3 ).'"
stroke-linejoin="round" stroke-linejoin="round"
stroke-linecap="round" stroke-linecap="round"
stroke="url(#SVGChart-fadeFromNothing-'.( $chartID ).')" stroke="url(#neatchart-fadeFromNothing-'.( $chartID ).')"
marker-end="url(#SVGChart-markerCircle-'.( $chartID ).')" marker-end="url(#neatchart-markerCircle-'.( $chartID ).')"
> >
<path d="'.( $this->options['smoothed'] ? $chartSplines : $chartPoints ).'" /> <path d="'.( $chartPoints ).'" />'.($this->options['yAxisZero'] ? '
<path
stroke="none"
fill="url(#neatchart-fadeFromNothing-'.( $chartID ).')"
fill-opacity="0.25"
marker-end="none"
d="'.$chartPoints.' L'.$this->width.','.$this->height.' 0,'.$this->height.' Z'.'" />' : '').'
</g> </g>
</g> </g>
</svg>'; </svg>';

View file

@ -10,7 +10,8 @@ namespace NeatCharts {
'smoothed' => false, 'smoothed' => false,
'fontSize' => 15, 'fontSize' => 15,
'yAxisEnabled'=>true, 'yAxisEnabled'=>true,
'xAxisEnabled'=>false 'xAxisEnabled'=>false,
'yAxisZero'=>false
]; ];
protected $width; protected $width;
@ -24,11 +25,6 @@ namespace NeatCharts {
protected $yRange; protected $yRange;
protected $padding = ['top'=>10, 'right'=>10, 'bottom'=>10, 'left'=>10]; protected $padding = ['top'=>10, 'right'=>10, 'bottom'=>10, 'left'=>10];
protected function arrayGet($array, $key, $default = NULL)
{
return isset($array[$key]) ? $array[$key] : $default;
}
protected function labelFormat($float, $places, $minPlaces = 0) { protected function labelFormat($float, $places, $minPlaces = 0) {
$value = number_format($float, max($minPlaces, $places)); $value = number_format($float, max($minPlaces, $places));
// add a trailing space if there's no decimal // add a trailing space if there's no decimal
@ -57,7 +53,29 @@ namespace NeatCharts {
return $precision; return $precision;
} }
public function __construct($chartData, $options = []) { protected function setWindow($chartData) {
end($chartData);
$this->xMax = key($chartData);
reset($chartData);
$this->xMin = key($chartData);
$this->xRange = $this->xMax - $this->xMin;
$this->yMin = ($this->options['yAxisZero'] ? 0 : INF);
$this->yMax = -INF;
foreach ($chartData as $x => $y) {
if ($y < $this->yMin) {
$this->yMin = $y;
$yMinX = $x;
}
if ($y > $this->yMax) {
$this->yMax = $y;
$yMaxX = $x;
}
}
$this->yRange = $this->yMax - $this->yMin;
}
final public function __construct($chartData, $options = []) {
$this->setOptions($options); $this->setOptions($options);
$this->setData($chartData); $this->setData($chartData);
} }