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

View file

@ -1,31 +1,33 @@
<?php
namespace NeatCharts {
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) {
$this->setWindow($chartData, $this->options); // sets min, max, range, etc
// we assume $chartData is sorted by key and keys and values are all numeric
$previousX = $previousY = null;
end($chartData);
$this->xMax = key($chartData);
reset($chartData);
$this->xMin = key($chartData);
$this->xRange = $this->xMax - $this->xMin;
$count = count($chartData);
$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 < $this->yMin) {
$this->yMin = $y;
$yMinX = $x;
}
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;
@ -36,7 +38,6 @@ namespace NeatCharts {
$previousY = $y;
$previousX = $x;
}
$this->yRange = $this->yMax - $this->yMin;
$averageAbsSlope /= $this->yRange * $deltaX; // turn this absolute-deltas total into a slope
if ($this->options['smoothed']) {
@ -140,15 +141,13 @@ namespace NeatCharts {
}
$chartPoints = 'M';
$chartSplines = 'M'.
$this->transformX($this->xMin).','.
$this->transformY($chartData[$this->xMin]);
if ($this->options['smoothed']) {
$chartPoints .= $this->transformX($this->xMin).','.$this->transformY($chartData[$this->xMin]);
foreach ($chartData as $x => $y) {
$controlX = $deltaX / 3 / sqrt(1 + $tangents[$x]**2);
$controlY = $tangents[$x] * $controlX;
if ($x != $this->xMin) {
$chartSplines .= ' S'.
$chartPoints .= ' S'.
$this->transformX($x - $controlX).','.
$this->transformY($y - $controlY).' '.
$this->transformX($x).','.
@ -166,31 +165,29 @@ namespace NeatCharts {
$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">
<defs>
<marker id="SVGChart-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'] ).';" />
<marker id="neatchart-markerCircle-'.( $chartID ).'" markerWidth="2" markerHeight="2" refX="1" refY="1" markerUnits="strokeWidth">
<circle class="neatchart-marker" cx="1" cy="1" r="1" stroke="none" fill="'.( $this->options['markerColor'] ).'" />
</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="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>
</linearGradient>
</defs>
<g class="SVGChart">'.( $this->options['yAxisEnabled'] || $this->options['xAxisEnabled'] ? '
<g class="neatchart">'.( $this->options['yAxisEnabled'] || $this->options['xAxisEnabled'] ? '
<g class="chart__gridLines"
shape-rendering="crispEdges"
fill="none"
stroke="'.( $this->options['labelColor'] ).'"
stroke-opacity="0.75"
stroke-width="1"
vector-effect="non-scaling-stroke"
stroke-dasharray="2, 2"
>
shape-rendering="crispEdges">
<path class="chart__gridLinePaths" d="'.( $gridLines ).'" />
</g>
<g class="chart__gridLabels"
fill="'.( $this->options['labelColor'] ).'"
font-family="monospace"
font-size="'.( $this->options['fontSize'] ).'px"
>
font-size="'.( $this->options['fontSize'] ).'px">
'.( $gridText ).'
</g>' : '').'
<g class="chart__plotLine"
@ -198,10 +195,16 @@ namespace NeatCharts {
stroke-width="'.( $this->options['fontSize'] / 3 ).'"
stroke-linejoin="round"
stroke-linecap="round"
stroke="url(#SVGChart-fadeFromNothing-'.( $chartID ).')"
marker-end="url(#SVGChart-markerCircle-'.( $chartID ).')"
stroke="url(#neatchart-fadeFromNothing-'.( $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>
</svg>';

View file

@ -10,7 +10,8 @@ namespace NeatCharts {
'smoothed' => false,
'fontSize' => 15,
'yAxisEnabled'=>true,
'xAxisEnabled'=>false
'xAxisEnabled'=>false,
'yAxisZero'=>false
];
protected $width;
@ -24,11 +25,6 @@ namespace NeatCharts {
protected $yRange;
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) {
$value = number_format($float, max($minPlaces, $places));
// add a trailing space if there's no decimal
@ -57,7 +53,29 @@ namespace NeatCharts {
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->setData($chartData);
}