PHP/JavaScript webapp to analyse spending habits
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

analyser.js 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. var previousPoint = null;
  2. var plots = {};
  3. var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
  4. function colourTableRows(table) {
  5. $('tr', table).removeClass('alt');
  6. $('tr:visible:even', table).addClass('alt');
  7. }
  8. function showTooltip(x, y, contents) {
  9. $('<div id="tooltip">' + contents + '</div>').css( {
  10. position: 'absolute',
  11. display: 'none',
  12. top: y + 5,
  13. left: x + 5,
  14. border: '1px solid #fdd',
  15. padding: '2px',
  16. 'background-color': '#fee',
  17. }).appendTo("body").fadeIn(200);
  18. }
  19. function getDateKey(date) {
  20. return date.getFullYear() + '-' + (date.getMonth() < 9 ? '0' : '') + (date.getMonth() + 1);
  21. }
  22. function showSelectedMonths(start, end, incoming, outgoing) {
  23. $('#historytable tr.data').remove();
  24. $('#historytable').show();
  25. var startDate = new Date(start), endDate = new Date(end);
  26. if (startDate.getDate() > 1) {
  27. startDate.setDate(1);
  28. startDate.getMonth() == 11 && startDate.setYear(startDate.getFullYear() + 1);
  29. startDate.setMonth(startDate.getMonth() == 11 ? 0 : startDate.getMonth() + 1);
  30. }
  31. var startKey = getDateKey(startDate), endKey = getDateKey(endDate);
  32. if (startKey == endKey) {
  33. $('#historytable h3').text('Transactions for ' + months[startDate.getMonth()] + ' ' + startDate.getFullYear());
  34. } else if (startDate.getFullYear() == endDate.getFullYear()) {
  35. $('#historytable h3').text('Transactions for ' + months[startDate.getMonth()] + '-' + months[endDate.getMonth()] + ' ' + startDate.getFullYear());
  36. } else {
  37. $('#historytable h3').text('Transactions for ' + months[startDate.getMonth()] + ' ' + startDate.getFullYear() + ' - ' + months[endDate.getMonth()] + ' ' + endDate.getFullYear());
  38. }
  39. var pieData = {};
  40. var table = $('#historytable table');
  41. var include = false;
  42. var lastEntry = {};
  43. var id = 0;
  44. $.each(data, function(month, monthData) {
  45. if (month == startKey) { include = true; }
  46. if (include) {
  47. $.each(monthData, function(index, trans) {
  48. if (incoming != trans.Amount > 0) { return; }
  49. var category = trans.Category ? trans.Category : 'Unsorted';
  50. var tr = $('<tr/>').addClass('data').addClass('category' + category.replace(/[^a-zA-Z]*/g, '')).appendTo(table);
  51. if (lastEntry.Description == trans.Description && lastEntry.Type == trans.Type && lastEntry.Category == lastEntry.Category) {
  52. tr.hide();
  53. if (lastEntry.id) {
  54. lastEntry.count++;
  55. $('span', lastEntry.tr).text('(+' + lastEntry.count + ')');
  56. } else {
  57. lastEntry.id = ++id;
  58. lastEntry.count = 1;
  59. var a = $('<span>').addClass('link').text('(+1)').appendTo($('td.desc', lastEntry.tr).append(' '));
  60. a.data('otherAmount', lastEntry.Amount);
  61. a.bind('click', { id: lastEntry.id, tr: lastEntry.tr }, function(event) {
  62. $('.hidden' + event.data.id).toggle();
  63. var text = $(this).text();
  64. text = (text.substr(0, 2) == '(+' ? '(-' : '(+') + text.substr(2);
  65. $(this).text(text);
  66. var amount = $('.amount', event.data.tr);
  67. var oldAmount = amount.text();
  68. amount.text($(this).data('otherAmount'));
  69. $(this).data('otherAmount', oldAmount);
  70. colourTableRows($('#historytable'));
  71. });
  72. }
  73. lastEntry.Amount = Math.round(100 * (lastEntry.Amount + trans.Amount)) / 100;
  74. $('.amount', lastEntry.tr).text(lastEntry.Amount);
  75. tr.addClass('collapsed hidden' + lastEntry.id);
  76. } else {
  77. lastEntry = trans;
  78. lastEntry.tr = tr;
  79. }
  80. $('<td/>').text(trans.Date.date.split(' ')[0]).appendTo(tr);
  81. $('<td/>').text(trans.Type ? trans.Type : 'Other').appendTo(tr);
  82. $('<td/>').text(trans.Category ? trans.Category : '').appendTo(tr);
  83. $('<td/>').addClass('desc').text(trans.Description).appendTo(tr);
  84. $('<td/>').addClass('amount').text(trans.Amount).appendTo(tr);
  85. if (category != '(Ignored)') {
  86. if (!pieData[category]) { pieData[category] = 0; }
  87. pieData[category] += Math.abs(trans.Amount);
  88. }
  89. });
  90. }
  91. if (month == endKey) { include = false; }
  92. });
  93. colourTableRows(table);
  94. var seriesData = [];
  95. $.each(pieData, function(category, amount) {
  96. seriesData.push({ label: category + ' (' + Math.round(amount) + ')', data: amount });
  97. });
  98. seriesData.sort(function(a, b) { return b.data - a.data; });
  99. plots.expense = $.plot($('#expense'), seriesData, {
  100. series: { pie: { show: true, innerRadius: 0.5, highlight: { opacity: 0.5 } } },
  101. grid: { clickable: true }
  102. });
  103. }
  104. $(function() {
  105. var transData = [{label: 'Income', data: []}, {label: 'Expense', data: []}, {label: 'Difference', data: []}];
  106. var categories = {};
  107. var min = new Date().getTime(), max = 0;
  108. $.each(data, function(month, entries) {
  109. var split = month.split('-');
  110. var timestamp = new Date(split[0], split[1] - 1).getTime();
  111. var sum = [0, 0];
  112. $.each(entries, function() {
  113. if (this.Category == '(Ignored)') { return; }
  114. if (this.Amount < 0) {
  115. var category = this.Category ? this.Category : 'Unsorted';
  116. if (!categories[category]) { categories[category] = {}; }
  117. if (!categories[category][timestamp]) { categories[category][timestamp] = 0; }
  118. categories[category][timestamp] -= this.Amount;
  119. }
  120. sum[this.Amount < 0 ? 1 : 0] += this.Amount;
  121. });
  122. transData[0].data.push([timestamp, sum[0]]);
  123. transData[1].data.push([timestamp, sum[1]]);
  124. transData[2].data.push([timestamp, sum[0] + sum[1]]);
  125. min = Math.min(min, timestamp);
  126. max = Math.max(max, timestamp);
  127. });
  128. var catData = [];
  129. $.each(categories, function(category, entries) {
  130. var series = {label: category, data: []};
  131. var total = 0;
  132. $.each(transData[0].data, function() {
  133. var timestamp = this[0];
  134. var val = entries[timestamp] ? entries[timestamp] : 0;
  135. total += val;
  136. series.data.push([timestamp, val]);
  137. });
  138. series.total = total;
  139. catData.push(series);
  140. });
  141. catData.sort(function(a, b) { return a.total - b.total; });
  142. plots.history = $.plot($('#history'), transData, {
  143. xaxis: { mode: 'time', timeformat: '%y/%m' },
  144. series: {
  145. lines: { show: true, fill: true },
  146. points: { show: true }
  147. },
  148. grid: {
  149. hoverable: true,
  150. clickable: true,
  151. markings: [{ color: '#000', lineWidth: 1, xaxis: { from: min, to: max }, yaxis: { from: 0, to: 0 } }]
  152. },
  153. selection: { mode : "x" }
  154. });
  155. plots.cathistory = $.plot($('#cathistory'), catData, {
  156. xaxis: { mode: 'time', timeformat: '%y/%m' },
  157. legend: { noColumns: 2 },
  158. series: {
  159. stack: true,
  160. lines: { show: true, fill: true }
  161. },
  162. });
  163. $("#history").bind("plothover", function (event, pos, item) {
  164. if (item) {
  165. var id = {dataIndex: item.dataIndex, seriesIndex: item.seriesIndex};
  166. if (previousPoint == null || previousPoint.dataIndex != id.dataIndex || previousPoint.seriesIndex != id.seriesIndex) {
  167. previousPoint = id;
  168. $("#tooltip").remove();
  169. var x = item.datapoint[0],
  170. y = item.datapoint[1].toFixed(2);
  171. var date = new Date(x);
  172. var seriesTitles = ["Money in", "Money out", "Balance change"];
  173. showTooltip(item.pageX, item.pageY, (seriesTitles[item.seriesIndex]) + " during " + months[date.getMonth()] + " " + date.getFullYear() + " = " + y);
  174. }
  175. } else {
  176. $("#tooltip").remove();
  177. previousPoint = null;
  178. }
  179. });
  180. $('#history').bind('plotselected', function(event, ranges) {
  181. var startDate = parseInt(ranges.xaxis.from.toFixed());
  182. var endDate = parseInt(ranges.xaxis.to.toFixed());
  183. $.history.load('start:' + startDate + ';end:' + endDate + ';type:expenses');
  184. });
  185. $('#history').bind('plotclick', function(event, pos, item) {
  186. if (item) {
  187. $.history.load('start:' + item.datapoint[0] + ';end:' + item.datapoint[0] + ';type:' + (item.seriesIndex == 0 ? 'income' : 'expenses'));
  188. }
  189. });
  190. $('#expense').bind('plotclick', function(event, pos, item) {
  191. $('#historytable .data').hide();
  192. $('#historytable .category' + item.series.label.replace(/[^a-zA-Z]*/g, '')).show();
  193. colourTableRows($('#historytable'));
  194. });
  195. $.history.init(function(hash) {
  196. var match = /start:([0-9]+);end:([0-9]+);type:(income|expenses)/.exec(hash);
  197. if (match != null) {
  198. showSelectedMonths(parseInt(match[1]), parseInt(match[2]), match[3] == 'income', match[3] == 'expenses');
  199. plots.history.setSelection({ xaxis: { from: parseInt(match[1]), to: parseInt(match[2]) }});
  200. }
  201. });
  202. });