diff --git a/README.md b/README.md
index 52c8cc6..6ff6414 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
-# Dash-SVG-chart
-PHP project to generate cached SVG price charts
+# neat-charts
+PHP project to generate clean-looking SVG price charts

+24h of Dash price in Bitcoin from Poloniex.com
## Requirements
@@ -14,20 +15,24 @@ Extract the files from https://github.com/seigler/Dash-SVG-chart/archive/master.
In your PHP file:
```php
+800,
'height'=>250,
'lineColor'=>"#1C75BC",
'labelColor'=>"#777",
'smoothed'=>false
]);
+print $chart->render();
```
In your HTML:
diff --git a/SVGChartBuilder.php b/SVGChartBuilder.php
deleted file mode 100644
index 30dee2a..0000000
--- a/SVGChartBuilder.php
+++ /dev/null
@@ -1,276 +0,0 @@
- 800,
- 'height' => 250,
- 'lineColor' => '#000',
- 'labelColor' => '#000',
- 'smoothed' => false,
- 'fontSize' => 15
- ];
-
- private $width;
- private $height;
- private $output;
- private $xMin;
- private $xMax;
- private $xRange;
- private $yMin;
- private $yMax;
- private $yRange;
- private $padding = ['top'=>10, 'right'=>10, 'bottom'=>10, 'left'=>10];
-
- private function arrayGet($array, $key, $default = NULL)
- {
- return isset($array[$key]) ? $array[$key] : $default;
- }
-
- private 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 */
- /* Transform data coords to chart coords */
- private function transformX($x) {
- return round(
- ($x - $this->xMin) / $this->xRange * $this->width
- , 2);
- }
- private function transformY($y) {
- return round(
- // SVG has y axis reversed, 0 is at the top
- ($this->yMax - $y) / $this->yRange * $this->height
- , 2);
- }
-
- private 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;
- }
-
- public function __construct($chartData, $options) {
- $this->setOptions($options);
- $this->setData($chartData);
- }
-
- public function setOptions($options) {
- $this->options = array_replace($this->options, $options);
- $this->padding['left'] = $this->options['fontSize'] * 5;
- $this->padding['top'] = $this->padding['bottom'] = $this->options['fontSize'];
- }
-
- public function setData($chartData) {
- // 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;
- }
- if ($x == $this->xMax) {
- $secants[$x] = ($y - $previousY) / $deltaX;
- }
- $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']) {
- // 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 == $this->xMax) {
- $tangents[$x] = $secant;
- }
- if ($x == $this->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));
- $this->width = $this->options['width'] - $this->padding['left'] - $this->padding['right'];
- if (isset($this->options['height'])) {
- $this->height = $this->options['height'] - $this->padding['top'] - $this->padding['bottom'];
- } else {
- $this->height = floor($aspectRatio * $this->width);
- $this->options['height'] = $this->height + $this->padding['top'] + $this->padding['bottom'];
- }
-
- $chartPoints = 'M';
- $chartSplines = 'M'.
- $this->transformX($this->xMin).','.
- $this->transformY($chartData[$this->xMin]);
- if ($this->options['smoothed']) {
- foreach ($chartData as $x => $y) {
- $controlX = $deltaX / 3 / sqrt(1 + $tangents[$x]**2);
- $controlY = $tangents[$x] * $controlX;
- if ($x != $this->xMin) {
- $chartSplines .= ' S'.
- $this->transformX($x - $controlX).','.
- $this->transformY($y - $controlY).' '.
- $this->transformX($x).','.
- $this->transformY($y);
- }
- }
- } else {
- foreach ($chartData as $x => $y) {
- $chartPoints .=
- $this->transformX($x).','.
- $this->transformY($y) . ' ';
- }
- }
-
- $numLabels = 2 + ceil($this->height / $this->options['fontSize'] / 6);
- $labelInterval = $this->yRange / $numLabels;
- $labelModulation = 10 ** (1 + floor(-log($this->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 = $this->getPrecision($labelInterval);
-
- // Top and bottom grid lines
- $gridLines =
- 'M10,0 '.$this->width.',0 '.
- ' M10,'.$this->height.','.$this->width.','.$this->height;
-
- // Top and bottom grid labels
- $gridText =
- ''.($this->labelFormat($this->yMax, $labelPrecision + 1)).'' .
- ''.($this->labelFormat($this->yMin, $labelPrecision + 1)).'';
-
- // Main labels and grid lines
- for (
- $labelY = $this->yMin - fmod($this->yMin, $labelInterval) + $labelInterval; // Start at the first "nice" Y value > min
- $labelY < $this->yMax; // Keep going until max
- $labelY += $labelInterval // Add Interval each iteration
- ) {
- $labelHeight = $this->transformY($labelY);
- if ( // label is not too close to the min or max
- $labelHeight < $this->height - 1.5 * $this->options['fontSize'] &&
- $labelHeight > $this->options['fontSize'] * 1.5
- ) {
- $gridText .= ''.$this->labelFormat($labelY, $labelPrecision).'';
- $gridLines .= ' M0,'.$labelHeight.' '.$this->width.','.$labelHeight;
- } else if ( // label is too close
- $labelHeight < $this->height - $this->options['fontSize'] * 0.75 &&
- $labelHeight > $this->options['fontSize'] * 0.75
- ) {
- $gridLines .= ' M'.( // move grid line over when it's very close to the min or max label
- $labelHeight < $this->height - $this->options['fontSize'] / 2 && $labelHeight > $this->options['fontSize'] / 2 ? 0 : $this->options['fontSize'] / 2
- ).','.$labelHeight.' '.$this->width.','.$labelHeight;
- }
- }
-
- $chartID = rand();
- $this->output = '
-';
- }
- public function render() {
- return $this->output;
- }
-}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..b110962
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,8 @@
+{
+ "name": "seigler/neat-charts",
+ "autoload": {
+ "psr-0" : {
+ "NeatCharts\\LineChart" : "src"
+ }
+ }
+}
diff --git a/buffer.php b/demo/buffer.php
similarity index 100%
rename from buffer.php
rename to demo/buffer.php
diff --git a/demo.php b/demo/index.php
similarity index 88%
rename from demo.php
rename to demo/index.php
index 0a9deb2..dc5c05f 100644
--- a/demo.php
+++ b/demo/index.php
@@ -22,7 +22,7 @@
- PHP SVG Chart Builder: chart demos
+ NeatCharts demo
@@ -34,10 +34,7 @@
Fake Stock Market Data
500,
"height"=>150,
"fontSize"=>10
@@ -80,7 +77,7 @@ for ($n = 0, $current = $start; $n < 12; $n++) {
$chartData[$n] = $current;
}
-$tempChart = new SVGChartBuilder($chartData, [
+$tempChart = new NeatCharts\LineChart($chartData, [
"width"=>700,
"height"=>400,
"lineColor"=>"#D00",
diff --git a/poloniex-dash-btc.php b/demo/poloniex-dash-btc.php
similarity index 89%
rename from poloniex-dash-btc.php
rename to demo/poloniex-dash-btc.php
index 6ba1e88..72fa0b8 100644
--- a/poloniex-dash-btc.php
+++ b/demo/poloniex-dash-btc.php
@@ -3,11 +3,8 @@
Header('Content-type: image/svg+xml; charset=utf-8');
Header('Content-Disposition: inline; filename="Dash-24h-chart-' . date('Y-m-d\THisT') . '.svg"');
-include 'buffer.php';
-
-spl_autoload_register(function ($class_name) {
- include $class_name . ".php";
-});
+require 'buffer.php';
+require '../src/NeatCharts/LineChart.php'; // really just use composer instead
function getJson($url) {
if (empty($url)) {
@@ -43,7 +40,7 @@ foreach ($last24h as $item) {
$chartData[$item->date] = $item->weightedAverage;
}
-$poloniexChart = new SVGChartBuilder($chartData, [
+$poloniexChart = new NeatCharts\LineChart($chartData, [
'width'=>800,
'height'=>250,
'lineColor'=>"#1C75BC", // Dash blue
diff --git a/src/NeatCharts/LineChart.php b/src/NeatCharts/LineChart.php
new file mode 100644
index 0000000..9aae92a
--- /dev/null
+++ b/src/NeatCharts/LineChart.php
@@ -0,0 +1,277 @@
+ 800,
+ 'height' => 250,
+ 'lineColor' => '#000',
+ 'labelColor' => '#000',
+ 'smoothed' => false,
+ 'fontSize' => 15
+ ];
+
+ private $width;
+ private $height;
+ private $output;
+ private $xMin;
+ private $xMax;
+ private $xRange;
+ private $yMin;
+ private $yMax;
+ private $yRange;
+ private $padding = ['top'=>10, 'right'=>10, 'bottom'=>10, 'left'=>10];
+
+ private function arrayGet($array, $key, $default = NULL)
+ {
+ return isset($array[$key]) ? $array[$key] : $default;
+ }
+
+ private 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 */
+ /* Transform data coords to chart coords */
+ private function transformX($x) {
+ return round(
+ ($x - $this->xMin) / $this->xRange * $this->width
+ , 2);
+ }
+ private function transformY($y) {
+ return round(
+ // SVG has y axis reversed, 0 is at the top
+ ($this->yMax - $y) / $this->yRange * $this->height
+ , 2);
+ }
+
+ private 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;
+ }
+
+ public function __construct($chartData, $options = []) {
+ $this->setOptions($options);
+ $this->setData($chartData);
+ }
+
+ public function setOptions($options) {
+ $this->options = array_replace($this->options, $options);
+ $this->padding['left'] = $this->options['fontSize'] * 5;
+ $this->padding['top'] = $this->padding['bottom'] = $this->options['fontSize'];
+ }
+
+ public function setData($chartData) {
+ // 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;
+ }
+ if ($x == $this->xMax) {
+ $secants[$x] = ($y - $previousY) / $deltaX;
+ }
+ $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']) {
+ // 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 == $this->xMax) {
+ $tangents[$x] = $secant;
+ }
+ if ($x == $this->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));
+ $this->width = $this->options['width'] - $this->padding['left'] - $this->padding['right'];
+ if (isset($this->options['height'])) {
+ $this->height = $this->options['height'] - $this->padding['top'] - $this->padding['bottom'];
+ } else {
+ $this->height = floor($aspectRatio * $this->width);
+ $this->options['height'] = $this->height + $this->padding['top'] + $this->padding['bottom'];
+ }
+
+ $chartPoints = 'M';
+ $chartSplines = 'M'.
+ $this->transformX($this->xMin).','.
+ $this->transformY($chartData[$this->xMin]);
+ if ($this->options['smoothed']) {
+ foreach ($chartData as $x => $y) {
+ $controlX = $deltaX / 3 / sqrt(1 + $tangents[$x]**2);
+ $controlY = $tangents[$x] * $controlX;
+ if ($x != $this->xMin) {
+ $chartSplines .= ' S'.
+ $this->transformX($x - $controlX).','.
+ $this->transformY($y - $controlY).' '.
+ $this->transformX($x).','.
+ $this->transformY($y);
+ }
+ }
+ } else {
+ foreach ($chartData as $x => $y) {
+ $chartPoints .=
+ $this->transformX($x).','.
+ $this->transformY($y) . ' ';
+ }
+ }
+
+ $numLabels = 2 + ceil($this->height / $this->options['fontSize'] / 6);
+ $labelInterval = $this->yRange / $numLabels;
+ $labelModulation = 10 ** (1 + floor(-log($this->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 = $this->getPrecision($labelInterval);
+
+ // Top and bottom grid lines
+ $gridLines =
+ 'M10,0 '.$this->width.',0 '.
+ ' M10,'.$this->height.','.$this->width.','.$this->height;
+
+ // Top and bottom grid labels
+ $gridText =
+ ''.($this->labelFormat($this->yMax, $labelPrecision + 1)).'' .
+ ''.($this->labelFormat($this->yMin, $labelPrecision + 1)).'';
+
+ // Main labels and grid lines
+ for (
+ $labelY = $this->yMin - fmod($this->yMin, $labelInterval) + $labelInterval; // Start at the first "nice" Y value > min
+ $labelY < $this->yMax; // Keep going until max
+ $labelY += $labelInterval // Add Interval each iteration
+ ) {
+ $labelHeight = $this->transformY($labelY);
+ if ( // label is not too close to the min or max
+ $labelHeight < $this->height - 1.5 * $this->options['fontSize'] &&
+ $labelHeight > $this->options['fontSize'] * 1.5
+ ) {
+ $gridText .= ''.$this->labelFormat($labelY, $labelPrecision).'';
+ $gridLines .= ' M0,'.$labelHeight.' '.$this->width.','.$labelHeight;
+ } else if ( // label is too close
+ $labelHeight < $this->height - $this->options['fontSize'] * 0.75 &&
+ $labelHeight > $this->options['fontSize'] * 0.75
+ ) {
+ $gridLines .= ' M'.( // move grid line over when it's very close to the min or max label
+ $labelHeight < $this->height - $this->options['fontSize'] / 2 && $labelHeight > $this->options['fontSize'] / 2 ? 0 : $this->options['fontSize'] / 2
+ ).','.$labelHeight.' '.$this->width.','.$labelHeight;
+ }
+ }
+
+ $chartID = rand();
+ $this->output = '
+ ';
+ }
+ public function render() {
+ return $this->output;
+ }
+ }
+}