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 9.2KB

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