From f137a08493bbba81de2a47cfc1ecfc5fcc667eca Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Jun 2015 20:39:50 +0200 Subject: [PATCH] Implemented some more charts. --- .../Account/ChartJsAccountChartGenerator.php | 1 - .../Budget/ChartJsBudgetChartGenerator.php | 75 ++- .../ChartJsCategoryChartGenerator.php | 51 +- .../Report/ChartJsReportChartGenerator.php | 87 +++ .../Controllers/Chart/BudgetController.php | 6 +- .../Controllers/Chart/CategoryController.php | 4 +- .../Controllers/Chart/ReportController.php | 4 +- app/Providers/FireflyServiceProvider.php | 2 +- public/js/Chart.StackedBar.js | 556 ++++++++++++++++++ public/js/charts.js | 68 ++- public/js/reports.js | 18 +- resources/twig/budgets/show.twig | 7 +- resources/twig/categories/show.twig | 14 +- resources/twig/reports/year.twig | 33 +- 14 files changed, 887 insertions(+), 39 deletions(-) create mode 100644 app/Generator/Chart/Report/ChartJsReportChartGenerator.php create mode 100644 public/js/Chart.StackedBar.js diff --git a/app/Generator/Chart/Account/ChartJsAccountChartGenerator.php b/app/Generator/Chart/Account/ChartJsAccountChartGenerator.php index b3b22e11b5..f1ef92aa97 100644 --- a/app/Generator/Chart/Account/ChartJsAccountChartGenerator.php +++ b/app/Generator/Chart/Account/ChartJsAccountChartGenerator.php @@ -101,7 +101,6 @@ class ChartJsAccountChartGenerator implements AccountChartGenerator ]; $current = clone $start; - $today = new Carbon; while ($end >= $current) { $data['labels'][] = $current->formatLocalized($format); diff --git a/app/Generator/Chart/Budget/ChartJsBudgetChartGenerator.php b/app/Generator/Chart/Budget/ChartJsBudgetChartGenerator.php index 2c0a3e0f2e..4759744124 100644 --- a/app/Generator/Chart/Budget/ChartJsBudgetChartGenerator.php +++ b/app/Generator/Chart/Budget/ChartJsBudgetChartGenerator.php @@ -3,7 +3,9 @@ namespace FireflyIII\Generator\Chart\Budget; +use Config; use Illuminate\Support\Collection; +use Preferences; /** * Class ChartJsBudgetChartGenerator @@ -20,6 +22,29 @@ class ChartJsBudgetChartGenerator implements BudgetChartGenerator */ public function budget(Collection $entries) { + $data = [ + 'count' => 1, + 'labels' => [], + 'datasets' => [ + [ + 'label' => 'Amount', + 'data' => [], + ] + ], + ]; + + // language: + $language = Preferences::get('language', 'en')->data; + $format = Config::get('firefly.month.' . $language); + + /** @var array $entry */ + foreach ($entries as $entry) { + $data['labels'][] = $entry[0]->formatLocalized($format); + $data['datasets'][0]['data'][] = $entry[1]; + + } + + return $data; } /** @@ -29,6 +54,7 @@ class ChartJsBudgetChartGenerator implements BudgetChartGenerator */ public function budgetLimit(Collection $entries) { + return $this->budget($entries); } /** @@ -55,14 +81,14 @@ class ChartJsBudgetChartGenerator implements BudgetChartGenerator $spent = []; $overspent = []; $amount = []; - $expenses = []; + $expenses = []; foreach ($entries as $entry) { if ($entry[1] != 0 || $entry[2] != 0 || $entry[3] != 0) { $left[] = round($entry[1], 2); $spent[] = round($entry[2], 2); $overspent[] = round($entry[3], 2); $amount[] = round($entry[4], 2); - $expenses[] = round($entry[5], 2); + $expenses[] = round($entry[5], 2); //$data['count']++; } } @@ -87,5 +113,50 @@ class ChartJsBudgetChartGenerator implements BudgetChartGenerator */ public function year(Collection $budgets, Collection $entries) { + // language: + $language = Preferences::get('language', 'en')->data; + $format = Config::get('firefly.month.' . $language); + + $data = [ + 'count' => 0, + 'labels' => [], + 'datasets' => [], + ]; + + foreach ($budgets as $budget) { + $data['labels'][] = $budget->name; + $data['count']++; + } + /** @var array $entry */ + foreach ($entries as $entry) { + $array = [ + 'label' => $entry[0]->formatLocalized($format), + 'data' => [], + ]; + array_shift($entry); + $array['data'] = $entry; + $data['datasets'][] = $array; + + } + + return $data; + + + $chart = new GChart; + // add columns: + $chart->addColumn(trans('firefly.month'), 'date'); + foreach ($budgets as $budget) { + $chart->addColumn($budget->name, 'number'); + } + + /** @var array $entry */ + foreach ($entries as $entry) { + + $chart->addRowArray($entry); + } + + $chart->generate(); + + return $chart->getData(); } } \ No newline at end of file diff --git a/app/Generator/Chart/Category/ChartJsCategoryChartGenerator.php b/app/Generator/Chart/Category/ChartJsCategoryChartGenerator.php index 3eea5b5218..74c004477a 100644 --- a/app/Generator/Chart/Category/ChartJsCategoryChartGenerator.php +++ b/app/Generator/Chart/Category/ChartJsCategoryChartGenerator.php @@ -2,7 +2,9 @@ namespace FireflyIII\Generator\Chart\Category; +use Config; use Illuminate\Support\Collection; +use Preferences; /** @@ -20,6 +22,27 @@ class ChartJsCategoryChartGenerator implements CategoryChartGenerator */ public function all(Collection $entries) { + // language: + $language = Preferences::get('language', 'en')->data; + $format = Config::get('firefly.month.' . $language); + + $data = [ + 'count' => 1, + 'labels' => [], + 'datasets' => [ + [ + 'label' => 'Spent', + 'data' => [] + ] + ], + ]; + + foreach ($entries as $entry) { + $data['labels'][] = $entry[0]->formatLocalized($format); + $data['datasets'][0]['data'][] = round($entry[1], 2); + } + + return $data; } /** @@ -42,7 +65,7 @@ class ChartJsCategoryChartGenerator implements CategoryChartGenerator foreach ($entries as $entry) { if ($entry['sum'] != 0) { $data['labels'][] = $entry['name']; - $data['datasets'][0]['data'][] = round($entry['sum'],2); + $data['datasets'][0]['data'][] = round($entry['sum'], 2); } } @@ -56,6 +79,8 @@ class ChartJsCategoryChartGenerator implements CategoryChartGenerator */ public function month(Collection $entries) { + return $this->all($entries); + } /** @@ -66,5 +91,29 @@ class ChartJsCategoryChartGenerator implements CategoryChartGenerator */ public function year(Collection $categories, Collection $entries) { + + // language: + $language = Preferences::get('language', 'en')->data; + $format = Config::get('firefly.month.' . $language); + + $data = [ + 'count' => 0, + 'labels' => [], + 'datasets' => [], + ]; + + foreach ($categories as $category) { + $data['labels'][] = $category->name; + } + + foreach ($entries as $entry) { + $date = $entry[0]->formatLocalized($format); + array_shift($entry); + $data['count']++; + $data['datasets'][] = ['label' => $date, 'data' => $entry]; + } + + return $data; + } } \ No newline at end of file diff --git a/app/Generator/Chart/Report/ChartJsReportChartGenerator.php b/app/Generator/Chart/Report/ChartJsReportChartGenerator.php new file mode 100644 index 0000000000..de51c0e6f2 --- /dev/null +++ b/app/Generator/Chart/Report/ChartJsReportChartGenerator.php @@ -0,0 +1,87 @@ +data; + $format = Config::get('firefly.month.' . $language); + + $data = [ + 'count' => 2, + 'labels' => [], + 'datasets' => [ + [ + 'label' => trans('firefly.income'), + 'data' => [] + ], + [ + 'label' => trans('firefly.expenses'), + 'data' => [] + ] + ], + ]; + + foreach ($entries as $entry) { + $data['labels'][] = $entry[0]->formatLocalized($format); + $data['datasets'][0]['data'][] = round($entry[1], 2); + $data['datasets'][1]['data'][] = round($entry[2], 2); + } + + return $data; + } + + /** + * @param string $income + * @param string $expense + * @param int $count + * + * @return array + */ + public function yearInOutSummarized($income, $expense, $count) + { + + // language: + $language = Preferences::get('language', 'en')->data; + $format = Config::get('firefly.month.' . $language); + + $data = [ + 'count' => 2, + 'labels' => [], + 'datasets' => [ + [ + 'label' => trans('firefly.income'), + 'data' => [] + ], + [ + 'label' => trans('firefly.expenses'), + 'data' => [] + ] + ], + ]; + $data['datasets'][0]['data'][] = round($income, 2); + $data['datasets'][1]['data'][] = round($expense, 2); + $data['datasets'][0]['data'][] = round(($income / $count), 2); + $data['datasets'][1]['data'][] = round(($expense / $count), 2); + return $data; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index edccfd042f..b63119a092 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -60,7 +60,7 @@ class BudgetController extends Controller $cache->addProperty($last); $cache->addProperty('budget'); if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore + //return Response::json($cache->get()); // @codeCoverageIgnore } $entries = new Collection; @@ -105,7 +105,7 @@ class BudgetController extends Controller $cache->addProperty($budget->id); $cache->addProperty($repetition->id); if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore + //return Response::json($cache->get()); // @codeCoverageIgnore } $entries = new Collection; @@ -215,7 +215,7 @@ class BudgetController extends Controller $cache->addProperty('budget'); $cache->addProperty('year'); if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore + //return Response::json($cache->get()); // @codeCoverageIgnore } $entries = new Collection; diff --git a/app/Http/Controllers/Chart/CategoryController.php b/app/Http/Controllers/Chart/CategoryController.php index cd8a01cfb3..57ca30bfd8 100644 --- a/app/Http/Controllers/Chart/CategoryController.php +++ b/app/Http/Controllers/Chart/CategoryController.php @@ -62,7 +62,7 @@ class CategoryController extends Controller $cache->addProperty('all'); $cache->addProperty('categories'); if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore + //return Response::json($cache->get()); // @codeCoverageIgnore } while ($start <= $end) { @@ -184,7 +184,7 @@ class CategoryController extends Controller $cache->addProperty('category'); $cache->addProperty('year'); if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore + //return Response::json($cache->get()); // @codeCoverageIgnore } $shared = $shared == 'shared' ? true : false; diff --git a/app/Http/Controllers/Chart/ReportController.php b/app/Http/Controllers/Chart/ReportController.php index 7e1b435e8d..120126dc59 100644 --- a/app/Http/Controllers/Chart/ReportController.php +++ b/app/Http/Controllers/Chart/ReportController.php @@ -55,7 +55,7 @@ class ReportController extends Controller $cache->addProperty($year); $cache->addProperty($shared); if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore + //return Response::json($cache->get()); // @codeCoverageIgnore } $entries = new Collection; @@ -95,7 +95,7 @@ class ReportController extends Controller $cache->addProperty($year); $cache->addProperty($shared); if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore + //return Response::json($cache->get()); // @codeCoverageIgnore } $start = new Carbon($year . '-01-01'); diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 857361af76..686953c1a7 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -107,8 +107,8 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind('FireflyIII\Generator\Chart\PiggyBank\PiggyBankChartGenerator', 'FireflyIII\Generator\Chart\PiggyBank\GooglePiggyBankChartGenerator'); //$this->app->bind('FireflyIII\Generator\Chart\PiggyBank\PiggyBankChartGenerator', 'FireflyIII\Generator\Chart\PiggyBank\GooglePiggyBankChartGenerator'); - $this->app->bind('FireflyIII\Generator\Chart\Report\ReportChartGenerator', 'FireflyIII\Generator\Chart\Report\GoogleReportChartGenerator'); //$this->app->bind('FireflyIII\Generator\Chart\Report\ReportChartGenerator', 'FireflyIII\Generator\Chart\Report\GoogleReportChartGenerator'); + $this->app->bind('FireflyIII\Generator\Chart\Report\ReportChartGenerator', 'FireflyIII\Generator\Chart\Report\ChartJsReportChartGenerator'); $this->app->bind('FireflyIII\Helpers\Help\HelpInterface', 'FireflyIII\Helpers\Help\Help'); diff --git a/public/js/Chart.StackedBar.js b/public/js/Chart.StackedBar.js new file mode 100644 index 0000000000..71b50ddf11 --- /dev/null +++ b/public/js/Chart.StackedBar.js @@ -0,0 +1,556 @@ +(function (factory) { + "use strict"; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['chart.js'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + module.exports = factory(require('chart.js')); + } else { + // Global browser + factory(Chart); + } +}(function (Chart) { + "use strict"; + + var helpers = Chart.helpers; + + var defaultConfig = { + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Boolean - Whether bars should be rendered on a percentage base + relativeBars : false, + + //String - A legend template + legendTemplate : "", + + //Boolean - Show total legend + showTotal: false, + + //String - Color of total legend + totalColor: '#fff', + + //String - Total Label + totalLabel: 'Total', + + //Boolean - Hide labels with value set to 0 + tooltipHideZero: false + }; + + Chart.Type.extend({ + name: "StackedBar", + defaults : defaultConfig, + initialize: function(data){ + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + // Save data as a source for updating of values & methods + this.data = data; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(barIndex){ + return this.calculateX(barIndex); + }, + calculateBarY : function(datasets, dsIndex, barIndex, value){ + var offset = 0, + sum = 0; + + for(var i = 0; i < datasets.length; i++) { + sum += datasets[i].bars[barIndex].value; + } + for(i = dsIndex; i < datasets.length; i++) { + if(i === dsIndex && value) { + offset += value; + } else { + offset = +offset + +datasets[i].bars[barIndex].value; + } + } + + if(options.relativeBars) { + offset = offset / sum * 100; + } + + return this.calculateY(offset); + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBaseHeight : function(){ + return (this.calculateY(1) - this.calculateY(0)); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + return this.calculateBaseWidth(); + }, + calculateBarHeight : function(datasets, dsIndex, barIndex, value) { + var sum = 0; + + for(var i = 0; i < datasets.length; i++) { + sum += datasets[i].bars[barIndex].value; + } + + if(!value) { + value = datasets[dsIndex].bars[barIndex].value; + } + + if(options.relativeBars) { + value = value / sum * 100; + } + + return this.calculateY(value); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + if(!helpers.isNumber(dataPoint)){ + dataPoint = 0; + } + //Add a new point for each piece of data, passing any required data to draw. + //Add 0 as value if !isNumber (e.g. empty values are useful when 0 values should be hidden in tooltip) + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + base: this.scale.endPoint, + height: 0, + width : this.scale.calculateBarWidth(this.datasets.length), + x: this.scale.calculateBarX(index), + y: this.scale.endPoint + }); + bar.save(); + }, this); + + this.render(); + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + helpers = Chart.helpers; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + helpers.each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = helpers.indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ + Elements.push(dataCollection[dataIndex]); + } + }); + + var total = { + datasetLabel: this.options.totalLabel, + value: 0, + fillColor: this.options.totalColor, + strokeColor: this.options.totalColor + }; + + helpers.each(Elements, function(element) { + if (this.options.tooltipHideZero && element.value === 0) { + return; + } + + xPositions.push(element.x); + yPositions.push(element.y); + + total.value += element.value; + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + if (this.options.showTotal) { + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, total)); + tooltipColors.push({ + fill: total.fillColor, + stroke: total.strokeColor + }); + } + + yMin = helpers.min(yPositions); + yMax = helpers.max(yPositions); + + xMin = helpers.min(xPositions); + xMax = helpers.max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx, + custom: this.options.customTooltips + }).draw(); + + } else { + helpers.each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: helpers.template(this.options.tooltipTemplate, Element), + chart: this.chart, + custom: this.options.customTooltips + }).draw(); + }, this); + } + } + return this; + }, + update : function(){ + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(this.data.datasets,function(dataset,datasetIndex){ + + helpers.extend(this.datasets[datasetIndex], { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + }); + + helpers.each(dataset.data,function(dataPoint,index){ + helpers.extend(this.datasets[datasetIndex].bars[index], { + value : dataPoint, + label : this.data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + }); + },this); + + },this); + + + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + helpers.each(self.datasets, function(dataset) { + helpers.each(dataset.bars, function(bar, barIndex) { + if(!values[barIndex]) values[barIndex] = 0; + if(self.options.relativeBars) { + values[barIndex] = 100; + } else { + values[barIndex] = +values[barIndex] + +bar.value; + } + }); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : this.options.xLabels || labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + if (helpers.isNumber(value)){ + //Add a new point for each piece of data, passing any required data to draw. + //Add 0 as value if !isNumber (e.g. empty values are useful when 0 values should be hidden in tooltip) + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : helpers.isNumber(value)?value:0, + label : label, + x: this.scale.calculateBarX(this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + } + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + var y = this.scale.calculateBarY(this.datasets, datasetIndex, index, bar.value), + height = this.scale.calculateBarHeight(this.datasets, datasetIndex, index, bar.value); + + //Transition then draw + if(bar.value > 0) { + bar.transition({ + base : this.scale.endPoint - (Math.abs(height) - Math.abs(y)), + x : this.scale.calculateBarX(index), + y : Math.abs(y), + height : Math.abs(height), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + },this); + } + }); +})); diff --git a/public/js/charts.js b/public/js/charts.js index 60bd4ecccb..cfd00f5d2f 100644 --- a/public/js/charts.js +++ b/public/js/charts.js @@ -4,22 +4,36 @@ Make some colours: */ /* -#555299 -#4285f4 -#db4437 -#f4b400 -#0f9d58 -#ab47bc -#00acc1 -#ff7043 -#9e9d24 -#5c6bc0", "#f06292", "#00796b", "#c2185b"], + #555299 + #4285f4 + # + # + # + # + # + # + # + #", "#", "#", "#"], */ var colourSet = [ [53, 124, 165], [0, 141, 76], [219, 139, 11], - [202, 25, 90] + [202, 25, 90], + [85, 82, 153], + [66, 133, 244], + [219, 68, 55], + [244, 180, 0], + [15, 157, 88], + [171, 71, 188], + [0, 172, 193], + [255, 112, 67], + [158, 157, 36], + [92, 107, 192], + [240, 98, 146], + [0, 121, 107], + [194, 24, 91], + ]; var fillColors = []; @@ -153,6 +167,7 @@ function columnChart(URL, container, options) { for (var i = 0; i < data.count; i++) { newData.labels = data.labels; var dataset = data.datasets[i]; + console.log('Now at index ' + i + ' (count is ' + fillColors.length + ')'); dataset.fillColor = fillColors[i]; dataset.strokeColor = strokePointHighColors[i]; dataset.pointColor = strokePointHighColors[i]; @@ -178,7 +193,36 @@ function columnChart(URL, container, options) { */ function stackedColumnChart(URL, container, options) { "use strict"; - columnChart(URL, container, options); + $.getJSON(URL).success(function (data) { + + var ctx = document.getElementById(container).getContext("2d"); + var newData = {}; + newData.datasets = []; + console.log('----'); + console.log('URL: ' + URL); + console.log('Total datasets: ' + data.datasets.length); + console.log('data.count: ' + data.count); + console.log('----'); + + for (var i = 0; i < data.count; i++) { + newData.labels = data.labels; + var dataset = data.datasets[i]; + console.log('Now at index ' + i + ' (count is ' + fillColors.length + ')'); + dataset.fillColor = fillColors[i]; + dataset.strokeColor = strokePointHighColors[i]; + dataset.pointColor = strokePointHighColors[i]; + dataset.pointStrokeColor = "#fff"; + dataset.pointHighlightFill = "#fff"; + dataset.pointHighlightStroke = strokePointHighColors[i]; + newData.datasets.push(dataset); + } + console.log(newData); + new Chart(ctx).StackedBar(newData, options); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); + console.log('URL for column chart : ' + URL); } /** diff --git a/public/js/reports.js b/public/js/reports.js index 563ea30237..0a790fa256 100644 --- a/public/js/reports.js +++ b/public/js/reports.js @@ -12,13 +12,17 @@ $(function () { function drawChart() { "use strict"; - columnChart('chart/report/in-out/' + year + shared, 'income-expenses-chart'); - columnChart('chart/report/in-out-sum/' + year + shared, 'income-expenses-sum-chart'); - - stackedColumnChart('chart/budget/year/' + year + shared, 'budgets'); - stackedColumnChart('chart/category/year/' + year + shared, 'categories'); - - lineChart('/chart/account/month/' + year + '/' + month + shared, 'account-balances-chart'); + if (typeof columnChart !== undefined) { + columnChart('chart/report/in-out/' + year + shared, 'income-expenses-chart'); + columnChart('chart/report/in-out-sum/' + year + shared, 'income-expenses-sum-chart'); + } + if (typeof stackedColumnChart !== undefined) { + stackedColumnChart('chart/budget/year/' + year + shared, 'budgets'); + stackedColumnChart('chart/category/year/' + year + shared, 'categories'); + } + if (typeof lineChart !== undefined && typeof month !== 'undefined') { + lineChart('/chart/account/month/' + year + '/' + month + shared, 'account-balances-chart'); + } } diff --git a/resources/twig/budgets/show.twig b/resources/twig/budgets/show.twig index 8dbf2a3800..cf2734d3f6 100644 --- a/resources/twig/budgets/show.twig +++ b/resources/twig/budgets/show.twig @@ -27,7 +27,12 @@
-
+ {% if Config.get('firefly.chart') == 'google' %} +
+ {% endif %} + {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}
diff --git a/resources/twig/categories/show.twig b/resources/twig/categories/show.twig index 32884ac3cb..771c5d56b8 100644 --- a/resources/twig/categories/show.twig +++ b/resources/twig/categories/show.twig @@ -12,7 +12,12 @@

{{ 'overview'|_ }} (month)

-
+ {% if Config.get('firefly.chart') == 'google' %} +
+ {% endif %} + {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}
@@ -22,7 +27,12 @@

{{ 'overview'|_ }} (all)

-
+ {% if Config.get('firefly.chart') == 'google' %} +
+ {% endif %} + {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}
diff --git a/resources/twig/reports/year.twig b/resources/twig/reports/year.twig index 3c072a7222..24c0c21d39 100644 --- a/resources/twig/reports/year.twig +++ b/resources/twig/reports/year.twig @@ -13,7 +13,12 @@

{{ 'incomeVsExpenses'|_ }}

-
+ {% if Config.get('firefly.chart') == 'google' %} +
+ {% endif %} + {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}
@@ -23,7 +28,12 @@

{{ 'incomeVsExpenses'|_ }}

-
+ {% if Config.get('firefly.chart') == 'google' %} +
+ {% endif %} + {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}
@@ -49,7 +59,12 @@

{{ 'categories'|_ }}

-
+ {% if Config.get('firefly.chart') == 'google' %} +
+ {% endif %} + {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}
@@ -61,7 +76,12 @@

{{ 'budgets'|_ }}

-
+ {% if Config.get('firefly.chart') == 'google' %} +
+ {% endif %} + {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}
@@ -76,12 +96,15 @@ {% endif %} {% if Config.get('firefly.chart') == 'chartjs' %} + + {% endif %}