PHP/JavaScript webapp to analyse spending habits
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

analyser.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. var previousPoint = null;
  2. var state = {};
  3. var oldState = {};
  4. var plots = {};
  5. // -----------------------------------------------------------------------------
  6. // ------------------ Date handling --------------------------------------------
  7. // -----------------------------------------------------------------------------
  8. Date.prototype.months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
  9. /**
  10. * Formats this date as year and two digit month, separated by a '-'.
  11. *
  12. * @return This date formatted as a key
  13. */
  14. Date.prototype.getKey = function() {
  15. return this.getFullYear() + '-' + (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1);
  16. }
  17. /**
  18. * Gets a textual representation of this date's month
  19. *
  20. * @return This date's month name
  21. */
  22. Date.prototype.getDisplayMonth = function() {
  23. return this.months[this.getMonth()];
  24. }
  25. /**
  26. * Gets a textual representation of the range of months between this date and
  27. * the specified other. It is assumed that the other date will occur after this.
  28. *
  29. * @param {Date} other The end date
  30. * @return {String} A textual representation of the date range
  31. */
  32. Date.prototype.getRangeText = function(other) {
  33. if (this.getFullYear() == other.getFullYear() && this.getMonth() == other.getMonth()) {
  34. return this.getDisplayMonth() + ' ' + this.getFullYear();
  35. } else if (this.getFullYear() == other.getFullYear()) {
  36. return this.getDisplayMonth() + '-' + other.getDisplayMonth() + ' ' + this.getFullYear();
  37. } else {
  38. return this.getDisplayMonth() + ' ' + this.getFullYear() + ' - ' + other.getDisplayMonth() + ' ' + other.getFullYear();
  39. }
  40. }
  41. /**
  42. * Parses a date string in the format YYYY-MM-DD HH:MM:SS.
  43. *
  44. * @param {String} input The input string
  45. * @return {int} Number of milliseconds since 1970
  46. */
  47. Date.parseYMD = function(input) {
  48. var parts = input.split(/[:\-\s]/);
  49. var date = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10), parseInt(parts[3], 10), parseInt(parts[4], 10), parseInt(parts[5], 10));
  50. return date.getTime();
  51. }
  52. /**
  53. * Gets a date object corresponding to the specified timestamp. If advanceToNext
  54. * is specified, and the timestamp doesn't already correspond to the first of
  55. * the month, the date is forwarded to the first day of the next month.
  56. *
  57. * @param {int} timestamp The timestamp to convert to a date
  58. * @param {bool} advanceToNext Whether to advance to the 1st or not
  59. * @return {Date} A corresponding date object
  60. */
  61. function getDate(timestamp, advanceToNext) {
  62. var date = new Date(timestamp);
  63. if (advanceToNext && date.getDate() > 1) {
  64. date.setDate(1);
  65. date.getMonth() == 11 && date.setYear(date.getFullYear() + 1);
  66. date.setMonth(date.getMonth() == 11 ? 0 : date.getMonth() + 1);
  67. }
  68. return date;
  69. }
  70. // -----------------------------------------------------------------------------
  71. // ------------------ Data handling --------------------------------------------
  72. // -----------------------------------------------------------------------------
  73. /**
  74. * Calculates the sum of transactions belonging to each category within
  75. * the specified data.
  76. *
  77. * @param data An array of transactions to include
  78. * @param {bool} incoming True to tally income, false to tally expenses
  79. * @return A mapping of categories to the sum of their transactions
  80. */
  81. function getCategoryTotals(data, incoming) {
  82. var catData = {};
  83. $.each(data, function() {
  84. trans = this;
  85. var category = trans.Category ? trans.Category : 'Unsorted';
  86. if (category != '(Ignored)' && incoming == trans.Amount > 0) {
  87. if (!catData[category]) { catData[category] = 0; }
  88. catData[category] += Math.abs(trans.Amount);
  89. }
  90. });
  91. return catData;
  92. }
  93. /**
  94. * Retrieves an array of transactions which occur between the specified two
  95. * dates. This has a resolution of a month -- any data in the same month
  96. * as the start date will be included, and any data after the month of the
  97. * end date will be excluded.
  98. *
  99. * @param {Date} start The date to start including data at
  100. * @param {Date} end The date to stop including data at
  101. * @return An array of transactions between the two dates
  102. */
  103. function getDataForRange(start, end) {
  104. var include = false;
  105. var included = [];
  106. $.each(data, function(month, monthData) {
  107. include |= month == start.getKey();
  108. if (include) {
  109. $.each(monthData, function(index, trans) {
  110. included.push(trans);
  111. });
  112. }
  113. include &= month != end.getKey();
  114. });
  115. return included;
  116. }
  117. // -----------------------------------------------------------------------------
  118. // ------------------ State handling -------------------------------------------
  119. // -----------------------------------------------------------------------------
  120. /**
  121. * Update the page's state with the specified new state. This causes the state
  122. * to be loaded into the user's history so they can use back and forward
  123. * functionality in their browser. New state is merged with the old state.
  124. *
  125. * @param newState The new properties to add to the state
  126. * @param invalidatedState An array of state keys to remove
  127. * @param invalidatedSubState An map of state subkeys to remove
  128. */
  129. function setState(newState, invalidatedState, invalidatedSubState) {
  130. oldState = $.extend(true, {}, state);
  131. $.extend(true, state, newState);
  132. invalidatedState && $.each(invalidatedState, function(_, x) { delete state[x]; });
  133. invalidatedSubState && $.each(invalidatedSubState, function(key, values) {
  134. $.each(values, function() {
  135. delete state[key][this];
  136. });
  137. });
  138. $.history.load(JSON.stringify(state));
  139. }
  140. /**
  141. * Called when the page state changes (either via a call to $.history.load or
  142. * by the user manually changing the fragment or going back or forward).
  143. *
  144. * @param {string} hash The new page fragment
  145. */
  146. function handleStateChange(hash) {
  147. try {
  148. state = JSON.parse(hash);
  149. } catch (ex) {
  150. state = {};
  151. }
  152. if (state.start && state.end && state.type) {
  153. if (state.start == oldState.start && state.end == oldState.end && state.type == oldState.type && state.categoryFilter == oldState.categoryFilter) {
  154. // Just show/hide nodes as required
  155. ensureExpanded(oldState.expanded, state.expanded);
  156. } else {
  157. // Update the transaction table and pie charts
  158. showSelectedMonths(state.start, state.end, state.type == 'income', state.type == 'expenses', state.categoryFilter, state.expanded);
  159. }
  160. // If the selection has changed, update the visual representation
  161. (oldState.start != state.start || oldState.end != state.end) && plots.history.setSelection({ xaxis: { from: state.start, to: state.end }});
  162. }
  163. }
  164. // -----------------------------------------------------------------------------
  165. /**
  166. * Formats the specified number in a manner suitable for a currency. That is,
  167. * fixed to two decimal places and with a thousand separator every 3 digits.
  168. *
  169. * @return A string representation of the number as a currency
  170. */
  171. Number.prototype.toCurrency = function() {
  172. return this.toFixed(2).replace(/([0-9])(?=([0-9]{3})+\.)/g, '$1,');
  173. };
  174. /**
  175. * Computes the arithmatic mean, variance and deviation for the given array.
  176. *
  177. * @param a Array of numbers to be averaged
  178. * @return A map containing the mean, variance and deviation
  179. */
  180. function getAverage(a){
  181. var r = {mean: 0, variance: 0, deviation: 0};
  182. var length = a.length;
  183. // Sum the array
  184. for (var sum = 0, i = length; i--; sum += a[i]);
  185. var mean = r.mean = sum / length
  186. // Sum the squares of the differences from the mean
  187. for (var i = length, sum = 0; i--; sum += Math.pow(a[i] - mean, 2));
  188. r.deviation = Math.sqrt(r.variance = sum / length)
  189. return r;
  190. }
  191. /**
  192. * Adds an 'alt' class to every other visible row in the specified table.
  193. *
  194. * @param table The table to be marked-up
  195. */
  196. function colourTableRows(table) {
  197. $('tr', table).removeClass('alt');
  198. $('tr:visible:even', table).addClass('alt');
  199. }
  200. /**
  201. * Shows a tooltip with the specified content at the given co-ordinates.
  202. *
  203. * @param {int} x The x co-ordinate to show the tooltip at
  204. * @param {int} y The y co-ordinate to show the tooltip at
  205. * @param contents The content to display in the tooltip element
  206. */
  207. function showTooltip(x, y, contents) {
  208. $('<div id="tooltip">' + contents + '</div>').css( {
  209. position: 'absolute',
  210. display: 'none',
  211. top: y + 5,
  212. left: x + 5,
  213. border: '1px solid #fdd',
  214. padding: '2px',
  215. 'background-color': '#fee',
  216. }).appendTo("body").fadeIn(200);
  217. }
  218. /**
  219. * Called when the user clicks on the expand/contract toggle on a transaction
  220. * line where similar entries have been merged.
  221. *
  222. * @param event The corresponding event
  223. */
  224. function expandLinkHandler(event) {
  225. var text = $(this).text();
  226. var expanded = text.substr(0, 2) == '(+';
  227. if (expanded) {
  228. var newExpanded = {};
  229. newExpanded[event.data.id] = true;
  230. setState({expanded: newExpanded}, []);
  231. } else {
  232. setState({}, [], {expanded: [event.data.id]});
  233. }
  234. colourTableRows($('#historytable'));
  235. return false;
  236. }
  237. /**
  238. * Ensures that the desired elements are appropriately expanded or collapsed.
  239. *
  240. * @param oldList A map containing keys for each entry that was previously expanded
  241. * @param newList A map containing keys for each entry that should now be expanded
  242. */
  243. function ensureExpanded(oldList, newList) {
  244. oldList = oldList || {};
  245. newList = newList || {};
  246. $.each(newList, function(id, _) {
  247. if (!oldList[id]) {
  248. // This entry needs to be expanded
  249. $('.hidden' + id).show();
  250. var handle = $('#collapseHandle' + id);
  251. handle.text(handle.text().replace(/\+/, '-'));
  252. handle.parents('tr').find('td.amount').text(parseFloat(handle.data('single')).toCurrency());
  253. }
  254. });
  255. $.each(oldList, function(id, _) {
  256. if (!newList[id]) {
  257. // This entry needs to be collapsed
  258. $('.hidden' + id).hide();
  259. var handle = $('#collapseHandle' + id);
  260. handle.text(handle.text().replace(/\-/, '+'));
  261. handle.parents('tr').find('td.amount').text(parseFloat(handle.data('total')).toCurrency());
  262. }
  263. });
  264. colourTableRows($('#historytable'));
  265. }
  266. /**
  267. * Determines if the two transactions should be merged together. That is,
  268. * whether the transactions have an identical description, type and category.
  269. *
  270. * @param a The first transaction
  271. * @param b The second transaction
  272. * @return True if the transactions should be merged, false otherwise
  273. */
  274. function shouldMerge(a, b) {
  275. return a.Description == b.Description && a.Type == b.Type && a.Category == b.Category;
  276. }
  277. /**
  278. * Draws a pie chart of transactions by category.
  279. *
  280. * @param included An array of transactions to include in the chart
  281. * @param incoming True to show income, false to show expenses
  282. */
  283. function drawCategoryPieChart(included, incoming) {
  284. var pieData = getCategoryTotals(included, incoming);
  285. var total = 0;
  286. $.each(pieData, function(_, amount) { total += amount; });
  287. var seriesData = [];
  288. $.each(pieData, function(category, amount) {
  289. seriesData.push({ label: category + ' (&pound;' + amount.toCurrency() + ', ' + Math.floor(100 * amount / total) + '%)', data: amount });
  290. });
  291. seriesData.sort(function(a, b) { return b.data - a.data; });
  292. plots.expense = $.plot($('#expense'), seriesData, {
  293. series: { pie: { show: true, innerRadius: 0.5, highlight: { opacity: 0.5 } } },
  294. grid: { clickable: true }
  295. });
  296. }
  297. /**
  298. * Calculates repeat transactions within the specified data.
  299. *
  300. * @param data The data to be analysed
  301. */
  302. function calculateRepeatTransactions(data) {
  303. $('#repeats').show();
  304. $('#repeats tr.data').remove();
  305. var table = $('#repeats table');
  306. // This assumes data is sorted by date
  307. var timeSpan = Date.parseYMD(data[data.length - 1].Date.date) - Date.parseYMD(data[0].Date.date);
  308. var descs = {};
  309. $.each(data, function() {
  310. if (!descs[this.Description]) { descs[this.Description] = []; }
  311. descs[this.Description].push(this);
  312. });
  313. var monthTotal = 0;
  314. $.each(descs, function(desc) {
  315. // We only care if there are at least more than 2
  316. if (this.length < 3) { return; }
  317. var lastTime = 0;
  318. var differences = [];
  319. var amounts = [];
  320. $.each(this, function() {
  321. var time = Date.parseYMD(this.Date.date);
  322. lastTime > 0 && differences.push(time - lastTime);
  323. lastTime = time;
  324. amounts.push(this.Amount);
  325. });
  326. var average = getAverage(differences);
  327. var averageAmount = getAverage(amounts);
  328. // I may have just made this metric up. Sue me.
  329. var stability = average.deviation / average.mean;
  330. var periodInDays = average.mean / (1000 * 60 * 60 * 24);
  331. var stretch = average.mean * differences.length / timeSpan;
  332. if (stretch > 0.5) {
  333. // Happens across a decent proportion of our timespan
  334. var monthValue, periodText, classes;
  335. if (stability < 0.5 && ((periodInDays >= 5 && periodInDays <= 9) || (periodInDays >= 27 && periodInDays <= 32))) {
  336. // Stable and roughly weekly or monthly
  337. monthValue = (periodInDays <= 9 ? 4 : 1) * averageAmount.mean;
  338. periodText = periodInDays <= 9 ? 'Weekly' : 'Monthly';
  339. classes = 'data';
  340. } else {
  341. // Somewhat sporadic
  342. monthValue = averageAmount.mean * 30.4 / periodInDays;
  343. periodText = 'Sporadic (~' + periodInDays.toFixed(1) + ' days)';
  344. classes = 'data sporadic';
  345. }
  346. var tr = $('<tr/>').addClass(classes).appendTo(table);
  347. $('<td/>').text(desc).appendTo(tr);
  348. $('<td/>').text(this[0].Category ? this[0].Category : 'Unsorted').appendTo(tr);
  349. $('<td/>').text(periodText).appendTo(tr);
  350. $('<td class="amount"/>').text(averageAmount.mean.toCurrency()).appendTo(tr);
  351. $('<td class="amount"/>').text(monthValue.toCurrency()).appendTo(tr);
  352. monthTotal += monthValue;
  353. }
  354. });
  355. colourTableRows(table);
  356. var tr = $('<tr/>').addClass('data total').appendTo(table);
  357. $('<th colspan="4" class="total">Total</th>').appendTo(tr);
  358. $('<td class="amount"></td>').text(monthTotal.toCurrency()).appendTo(tr);
  359. }
  360. /**
  361. * Displays transactions and draws a category pie chart for the specified
  362. * date range. Note that dates have a granularity of a month.
  363. *
  364. * @param {int} start The timestamp to start including transactions from
  365. * @param {int} end The timestamp to stop including transactions at
  366. * @param {bool} incoming Whether or not to include incoming transactions (income)
  367. * @param {bool} outgoing Whether or not to include outgoing transactions (expenses)
  368. * @param {string} categoryFilter The category to filter transactions to (or null)
  369. * @param expanded An object containing entries indicating which merged
  370. * transactions should be shown as expanded
  371. */
  372. function showSelectedMonths(start, end, incoming, outgoing, categoryFilter, expanded) {
  373. $('#historytable tr.data').remove();
  374. $('#historytable').show();
  375. expanded = expanded || [];
  376. var startDate = getDate(start, 1), endDate = getDate(end);
  377. $('#historytable h3').text((categoryFilter ? categoryFilter + ' t' : 'T') + 'ransactions for ' + startDate.getRangeText(endDate));
  378. var included = getDataForRange(startDate, endDate);
  379. var filtered = $.grep(included, function(x) {
  380. var category = x.Category ? x.Category : 'Unsorted';
  381. return (incoming == x.Amount > 0) && (!categoryFilter || categoryFilter == category);
  382. });
  383. var table = $('#historytable table');
  384. var total = 0;
  385. var lastEntry = {};
  386. var id = 0;
  387. $.each(filtered, function() {
  388. total += this.Amount;
  389. var category = this.Category ? this.Category : 'Unsorted';
  390. var tr = $('<tr/>').addClass('data').addClass('category' + category.replace(/[^a-zA-Z]*/g, '')).appendTo(table);
  391. if (shouldMerge(lastEntry, this)) {
  392. if (lastEntry.id) {
  393. var prefix = '(' + (expanded[lastEntry.id] ? '-' : '+');
  394. lastEntry.count++;
  395. $('span', lastEntry.tr).text(prefix + lastEntry.count + ')');
  396. } else {
  397. lastEntry.id = ++id;
  398. lastEntry.count = 1;
  399. var prefix = '(' + (expanded[lastEntry.id] ? '-' : '+');
  400. var a = $('<span>').addClass('link').text(prefix + '1)').attr('id', 'collapseHandle' + lastEntry.id).appendTo($('td.desc', lastEntry.tr).append(' '));
  401. a.bind('click', { id: lastEntry.id }, expandLinkHandler);
  402. a.data('single', lastEntry.Amount);
  403. }
  404. lastEntry.Amount = Math.round(100 * (lastEntry.Amount + this.Amount)) / 100;
  405. $('#collapseHandle' + lastEntry.id).data('total', lastEntry.Amount);
  406. !expanded[lastEntry.id] && tr.hide() && $('.amount', lastEntry.tr).text(lastEntry.Amount.toCurrency());
  407. tr.addClass('collapsed hidden' + lastEntry.id);
  408. } else {
  409. lastEntry = $.extend({}, this, {tr: tr});
  410. }
  411. $('<td/>').text(this.Date.date.split(' ')[0]).appendTo(tr);
  412. $('<td/>').text(this.Type ? this.Type : 'Other').appendTo(tr);
  413. $('<td/>').text(this.Category ? this.Category : '').appendTo(tr);
  414. $('<td/>').addClass('desc').text(this.Description).appendTo(tr);
  415. $('<td/>').addClass('amount').text(this.Amount.toCurrency()).appendTo(tr);
  416. });
  417. var tr = $('<tr/>').addClass('data total').appendTo(table);
  418. $('<th colspan="4" class="total">Total</th>').appendTo(tr);
  419. $('<td class="amount"></td>').text(total.toCurrency()).appendTo(tr);
  420. colourTableRows(table);
  421. drawCategoryPieChart(included, incoming);
  422. calculateRepeatTransactions(included);
  423. }
  424. $(function() {
  425. var transData = [{label: 'Income', data: []}, {label: 'Expense', data: []}, {label: 'Difference', data: []}];
  426. var categories = {};
  427. var min = new Date().getTime(), max = 0;
  428. $.each(data, function(month, entries) {
  429. var split = month.split('-');
  430. var timestamp = new Date(split[0], split[1] - 1).getTime();
  431. var sum = [0, 0];
  432. $.each(entries, function() {
  433. if (this.Category == '(Ignored)') { return; }
  434. if (this.Amount < 0) {
  435. var category = this.Category ? this.Category : 'Unsorted';
  436. if (!categories[category]) { categories[category] = {}; }
  437. if (!categories[category][timestamp]) { categories[category][timestamp] = 0; }
  438. categories[category][timestamp] -= this.Amount;
  439. }
  440. sum[this.Amount < 0 ? 1 : 0] += this.Amount;
  441. });
  442. transData[0].data.push([timestamp, sum[0]]);
  443. transData[1].data.push([timestamp, sum[1]]);
  444. transData[2].data.push([timestamp, sum[0] + sum[1]]);
  445. min = Math.min(min, timestamp);
  446. max = Math.max(max, timestamp);
  447. });
  448. var catData = [];
  449. $.each(categories, function(category, entries) {
  450. var series = {label: category, data: []};
  451. var total = 0;
  452. $.each(transData[0].data, function() {
  453. var timestamp = this[0];
  454. var val = entries[timestamp] ? entries[timestamp] : 0;
  455. total += val;
  456. series.data.push([timestamp, val]);
  457. });
  458. series.total = total;
  459. catData.push(series);
  460. });
  461. var markings = [];
  462. // Add a marking for each year division
  463. var year = new Date(new Date(max).getFullYear(), 0);
  464. while (year.getTime() > min) {
  465. markings.push({ color: '#000', lineWidth: 1, xaxis: { from: year.getTime(), to: year.getTime() } });
  466. year.setFullYear(year.getFullYear() - 1);
  467. }
  468. catData.sort(function(a, b) { return a.total - b.total; });
  469. plots.cathistory = $.plot($('#cathistory'), catData, {
  470. xaxis: { mode: 'time', timeformat: '%y/%m' },
  471. legend: { noColumns: 2 },
  472. series: {
  473. stack: true,
  474. lines: { show: true, fill: true }
  475. },
  476. grid: {
  477. markings: markings
  478. }
  479. });
  480. markings.push({ color: '#000', lineWidth: 1, yaxis: { from: 0, to: 0 } });
  481. plots.history = $.plot($('#history'), transData, {
  482. xaxis: { mode: 'time', timeformat: '%y/%m' },
  483. series: {
  484. lines: { show: true, fill: true },
  485. points: { show: true }
  486. },
  487. legend: { noColumns: 3, position: 'nw' },
  488. grid: {
  489. hoverable: true,
  490. clickable: true,
  491. markings: markings
  492. },
  493. selection: { mode : "x" }
  494. });
  495. $("#history").bind("plothover", function (event, pos, item) {
  496. if (item) {
  497. var id = {dataIndex: item.dataIndex, seriesIndex: item.seriesIndex};
  498. if (previousPoint == null || previousPoint.dataIndex != id.dataIndex || previousPoint.seriesIndex != id.seriesIndex) {
  499. previousPoint = id;
  500. $("#tooltip").remove();
  501. var x = item.datapoint[0],
  502. y = item.datapoint[1].toFixed(2);
  503. var date = new Date(x);
  504. var seriesTitles = ["Money in", "Money out", "Balance change"];
  505. showTooltip(item.pageX, item.pageY, (seriesTitles[item.seriesIndex]) + " during " + date.getDisplayMonth() + " " + date.getFullYear() + " = " + y);
  506. }
  507. } else {
  508. $("#tooltip").remove();
  509. previousPoint = null;
  510. }
  511. });
  512. $('#history').bind('plotselected', function(event, ranges) {
  513. var startDate = parseInt(ranges.xaxis.from.toFixed());
  514. var endDate = parseInt(ranges.xaxis.to.toFixed());
  515. if (state.start != startDate || state.end != endDate || state.type != 'expenses') {
  516. setState({ start: startDate, end: endDate, type: 'expenses' }, ['categoryFilter', 'expanded']);
  517. }
  518. });
  519. $('#history').bind('plotclick', function(event, pos, item) {
  520. if (item) {
  521. setState({ start: item.datapoint[0], end: item.datapoint[0], type: item.seriesIndex == 0 ? 'income' : 'expenses' }, ['categoryFilter', 'expanded']);
  522. }
  523. });
  524. $('#expense').bind('plotclick', function(event, pos, item) {
  525. setState({ categoryFilter: item.series.label.replace(/ \(.*$/, '') }, ['expanded']);
  526. });
  527. $.history.init(handleStateChange);
  528. });