PageRenderTime 60ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/js/app.js

https://github.com/open-city/look-at-cook
JavaScript | 869 lines | 814 code | 33 blank | 22 comment | 101 complexity | 4bc2c936cc108b5c02a1a2b92ecac608 MD5 | raw file
  1. (function(){
  2. var app = {}
  3. // Configuration variables to set
  4. startYear = 1993; // first year of budget data
  5. endYear = 2017; // last year of budget data
  6. activeYear = 2016; // default year to select
  7. debugMode = false; // change to true for debugging message in the javascript console
  8. municipalityName = 'Cook County Budget'; // name of budget municipality
  9. apropTitle = 'Appropriations'; // label for first chart line
  10. expendTitle = 'Expenditures'; // label for second chart line
  11. // CSV data source for budget data
  12. dataSource = '/data/cook_county_budget_cleaned.csv';
  13. app.GlobalChartOpts = {
  14. apropColor: '#264870',
  15. apropSymbol: 'circle',
  16. expendColor: '#7d9abb',
  17. expendSybmol: 'square',
  18. apropTitle: apropTitle,
  19. expendTitle: expendTitle,
  20. pointInterval: 365 * 24 * 3600 * 1000 // chart interval set to one year (in ms)
  21. }
  22. app.MainChartModel = Backbone.Model.extend({
  23. setYear: function(year, index){
  24. var exp = this.get('expenditures');
  25. var approp = this.get('appropriations');
  26. var expChange = BudgetHelpers.calc_change(exp[index], exp[index -1]);
  27. var appropChange = BudgetHelpers.calc_change(approp[index], approp[index - 1]);
  28. this.set({
  29. 'selectedExp': accounting.formatMoney(exp[index]),
  30. 'selectedApprop': accounting.formatMoney(approp[index]),
  31. 'expChange': expChange,
  32. 'appropChange': appropChange,
  33. 'viewYear': year,
  34. 'prevYear': year - 1
  35. });
  36. }
  37. });
  38. app.BreakdownRow = Backbone.Model.extend({
  39. yearIndex: null
  40. });
  41. app.BreakdownColl = Backbone.Collection.extend({
  42. setRows: function(year, index){
  43. var self = this;
  44. $.each(this.models, function(i, row){
  45. var query = {}
  46. query[row.get('type')] = row.get('rowName')
  47. var summ = collection.getSummary(row.get('type'), query, year)
  48. row.set(summ);
  49. row.yearIndex = index;
  50. });
  51. var max_app = _.max(this.models, function(obj){return obj.get('appropriations')});
  52. var max_exp = _.max(this.models, function(obj){return obj.get('expenditures')});
  53. var maxes = [max_app.get('appropriations'), max_exp.get('expenditures')];
  54. this.maxNum = maxes.sort(function(a,b){return b-a})[0];
  55. $.each(this.models, function(i, row){
  56. var apps = row.get('appropriations');
  57. var exps = row.get('expenditures');
  58. var app_perc = parseFloat((apps/self.maxNum) * 100) + '%';
  59. var exp_perc = parseFloat((exps/self.maxNum) * 100) + '%';
  60. row.set({app_perc:app_perc, exp_perc:exp_perc});
  61. });
  62. }
  63. });
  64. app.BudgetColl = Backbone.Collection.extend({
  65. startYear: startYear,
  66. endYear: endYear,
  67. activeYear: activeYear,
  68. updateYear: function(year, yearIndex){
  69. var expanded = [];
  70. $.each($('tr.expanded-content'), function(i, row){
  71. var name = $(row).prev().find('a.rowName').text();
  72. expanded.push(name);
  73. $(row).remove();
  74. })
  75. this.mainChartData.setYear(year, yearIndex);
  76. this.breakdownChartData.setRows(year, yearIndex);
  77. this.dataTable.fnDestroy();
  78. this.initDataTable();
  79. $.each(expanded, function(i, name){
  80. var sel = 'a.details:contains("' + name + '")';
  81. $(sel).first().trigger('click');
  82. })
  83. },
  84. updateTables: function(view, title, filter, year){
  85. // Various cleanup is needed when running this a second time.
  86. if(typeof this.mainChartView !== 'undefined'){
  87. this.mainChartView.undelegateEvents();
  88. }
  89. if($('#breakdown-table-body').html() != ''){
  90. $('#breakdown-table-body').empty();
  91. }
  92. if(typeof this.dataTable !== 'undefined'){
  93. this.dataTable.fnClearTable();
  94. this.dataTable.fnDestroy();
  95. }
  96. // Need to orientate the views to a top level
  97. if(typeof this.hierarchy[view] !== 'undefined'){
  98. this.topLevelView = view
  99. } else {
  100. this.bdView = view;
  101. }
  102. $('#secondary-title').text(this.topLevelView);
  103. if (typeof year === 'undefined'){
  104. year = this.activeYear;
  105. }
  106. var exp = [];
  107. var approp = [];
  108. var self = this;
  109. var values = this.toJSON();
  110. if (debugMode == true){
  111. console.log("Update Tables");
  112. console.log(this);
  113. }
  114. var incomingFilter = false;
  115. if (typeof filter !== 'undefined'){
  116. values = _.where(this.toJSON(), filter);
  117. incomingFilter = true;
  118. }
  119. var yearRange = this.getYearRange()
  120. $.each(yearRange, function(i, year){
  121. exp.push(self.getTotals(values, expendTitle, year));
  122. approp.push(self.getTotals(values, apropTitle, year));
  123. });
  124. var yearIndex = yearRange.indexOf(parseInt(year))
  125. var selExp = exp[yearIndex];
  126. var prevExp = exp[yearIndex - 1];
  127. var expChange = BudgetHelpers.calc_change(selExp, prevExp);
  128. var selApprop = approp[yearIndex];
  129. var prevApprop = approp[yearIndex - 1];
  130. var appropChange = BudgetHelpers.calc_change(selApprop, prevApprop);
  131. this.mainChartData = new app.MainChartModel({
  132. expenditures: exp,
  133. appropriations: approp,
  134. title: title,
  135. viewYear: year,
  136. prevYear: year - 1,
  137. selectedExp: accounting.formatMoney(selExp),
  138. selectedApprop: accounting.formatMoney(selApprop),
  139. appropChange: appropChange,
  140. expChange: expChange,
  141. view: self.topLevelView
  142. });
  143. var bd = []
  144. var chartGuts = this.pluck(view).getUnique();
  145. var all_nums = []
  146. $.each(chartGuts, function(i, name){
  147. if (!incomingFilter){
  148. filter = {}
  149. }
  150. filter[view] = name;
  151. var summary = self.getSummary(view, filter, year);
  152. if (summary){
  153. var row = new app.BreakdownRow(summary);
  154. bd.push(row);
  155. all_nums.push(summary['expenditures']);
  156. all_nums.push(summary['appropriations']);
  157. }
  158. });
  159. if (debugMode == true) console.log("all breakdown numbers: " + all_nums);
  160. var maxNum = all_nums.sort(function(a,b){return b-a})[0];
  161. this.breakdownChartData = new app.BreakdownColl(bd);
  162. this.breakdownChartData.maxNum = maxNum;
  163. if (debugMode == true) console.log("max bar chart num: " + maxNum);
  164. this.breakdownChartData.forEach(function(row){
  165. var exps = accounting.unformat(row.get('expenditures'));
  166. var apps = accounting.unformat(row.get('appropriations'));
  167. var exp_perc = parseFloat((exps/maxNum) * 100) + '%';
  168. var app_perc = parseFloat((apps/maxNum) * 100) + '%';
  169. row.set({app_perc:app_perc, exp_perc:exp_perc});
  170. var rowView = new app.BreakdownSummary({model:row});
  171. $('#breakdown-table-body').append(rowView.render().el);
  172. });
  173. this.mainChartView = new app.MainChartView({
  174. model: self.mainChartData
  175. });
  176. this.initDataTable();
  177. },
  178. initDataTable: function(){
  179. this.dataTable = $("#breakdown").dataTable({
  180. "aaSorting": [[1, "desc"]],
  181. "aoColumns": [
  182. null,
  183. {'sType': 'currency'},
  184. {'sType': 'currency'},
  185. null
  186. ],
  187. "bFilter": false,
  188. "bInfo": false,
  189. "bPaginate": false,
  190. "bRetrieve": true,
  191. "bAutoWidth": false
  192. });
  193. },
  194. bootstrap: function(init, year){
  195. var self = this;
  196. this.spin('#main-chart', 'large');
  197. $('#download-button').attr('href', dataSource);
  198. $.when($.get(dataSource)).then(
  199. function(data){
  200. var json = $.csv.toObjects(data);
  201. if (debugMode == true){
  202. console.log("Data source to object");
  203. console.log(data);
  204. }
  205. var loadit = []
  206. $.each(json, function(i, j){
  207. if (debugMode == true){
  208. console.log("Process row");
  209. console.log(j);
  210. }
  211. j['Fund Slug'] = BudgetHelpers.convertToSlug(j['Fund']);
  212. j['Department Slug'] = BudgetHelpers.convertToSlug(j['Department']);
  213. j['Control Officer Slug'] = BudgetHelpers.convertToSlug(j['Control Officer']);
  214. loadit.push(j)
  215. });
  216. self.reset(loadit);
  217. if (debugMode == true){
  218. console.log("Reset loadit");
  219. console.log(loadit);
  220. }
  221. self.hierarchy = {
  222. Fund: ['Fund', 'Department'],
  223. "Control Officer": ['Control Officer', 'Department']
  224. }
  225. if (typeof init === 'undefined'){
  226. self.topLevelView = 'Fund';
  227. if (!year){
  228. year = activeYear;
  229. }
  230. self.updateTables('Fund', municipalityName, undefined, year);
  231. } else {
  232. self.topLevelView = init[0];
  233. var lowerView = init[0];
  234. var name = init[1];
  235. var filter = {}
  236. var key = init[0] + ' Slug'
  237. filter[key] = name;
  238. var title = self.findWhere(filter).get(init[0])
  239. if (init.length == 2){
  240. lowerView = 'Department';
  241. }
  242. if(init.length > 2){
  243. name = init[2];
  244. lowerView = 'Expense Line';
  245. filter['Department Slug'] = name;
  246. title = self.findWhere(filter).get('Department');
  247. }
  248. self.updateTables(lowerView, title, filter, year);
  249. }
  250. // self.searchView = new app.SearchView();
  251. }
  252. );
  253. },
  254. spin: function(element, option){
  255. // option is either size of spinner or false to cancel it
  256. $(element).spin(option);
  257. },
  258. // Returns an array of valid years.
  259. getYearRange: function(){
  260. return Number.range(this.startYear, this.endYear + 1);
  261. },
  262. reduceTotals: function(totals){
  263. return totals.reduce(function(a,b){
  264. var int_a = parseFloat(a);
  265. var int_b = parseFloat(b);
  266. return int_a + int_b;
  267. });
  268. },
  269. // Returns a total for a given category and year
  270. // Example: "Expenditures 1995"
  271. getTotals: function(values, category, year){
  272. var all = _.pluck(values, category + ' ' + year);
  273. return this.reduceTotals(all);
  274. },
  275. getChartTotals: function(category, rows, year){
  276. var totals = [];
  277. $.each(rows, function(i, row){
  278. var attr = category + ' ' + year
  279. var val = row.get(attr);
  280. totals.push(parseInt(val));
  281. });
  282. return totals;
  283. },
  284. getSummary: function(view, query, year){
  285. if (typeof year === 'undefined'){
  286. year = this.activeYear;
  287. }
  288. var guts = this.where(query);
  289. if (guts.length < 1) {
  290. return null;
  291. }
  292. var summary = {};
  293. var self = this;
  294. var exp = self.getChartTotals(expendTitle, guts, year);
  295. var approp = self.getChartTotals(apropTitle, guts, year);
  296. var prevExp = self.getChartTotals(expendTitle, guts, year - 1);
  297. var prevApprop = self.getChartTotals(apropTitle, guts, year - 1);
  298. var expChange = BudgetHelpers.calc_change(self.reduceTotals(exp), self.reduceTotals(prevExp));
  299. var appropChange = BudgetHelpers.calc_change(self.reduceTotals(approp), self.reduceTotals(prevApprop));
  300. var self = this;
  301. $.each(guts, function(i, item){
  302. summary['rowName'] = item.get(view);
  303. summary['prevYear'] = year - 1;
  304. summary['year'] = year;
  305. summary['description'] = item.get(view + ' Description');
  306. summary['expenditures'] = self.reduceTotals(exp);
  307. summary['appropriations'] = self.reduceTotals(approp);
  308. summary['expChange'] = expChange;
  309. summary['appropChange'] = appropChange;
  310. summary['rowId'] = item.get(view + ' ID');
  311. summary['type'] = view
  312. // 'Link to Website' column refers to department website, not fund or control officer
  313. if (view == 'Department'){
  314. summary['link'] = item.get('Link to Website');
  315. }
  316. var hierarchy = self.hierarchy[self.topLevelView]
  317. var ranking = hierarchy.indexOf(view)
  318. if (ranking == 0){
  319. summary['child'] = hierarchy[1];
  320. summary['parent_type'] = null;
  321. } else if(ranking == 1){
  322. summary['child'] = hierarchy[2];
  323. summary['parent_type'] = hierarchy[0];
  324. } else if(ranking == 2) {
  325. summary['child'] = null;
  326. summary['parent_type'] = hierarchy[1];
  327. }
  328. if(summary['parent_type']){
  329. summary['parent'] = self.mainChartData.get('title')
  330. }
  331. summary['slug'] = item.get(view + ' Slug');
  332. });
  333. if (typeof summary['expenditures'] !== 'undefined'){
  334. return summary
  335. } else {
  336. return null
  337. }
  338. }
  339. });
  340. app.MainChartView = Backbone.View.extend({
  341. el: $('#main-chart'),
  342. // The bulk of the chart options are defined in the budget_highcharts.js file
  343. // and attached to the window over there. Dunno if that's the best approach but it works
  344. chartOpts: window.mainChartOpts,
  345. events: {
  346. 'click .breakdown-choice': 'breakIt'
  347. },
  348. // Render the view when you initialize it.
  349. initialize: function(){
  350. this._modelBinder = new Backbone.ModelBinder();
  351. this.render();
  352. this.updateCrumbs();
  353. this.model.on('change', function(model){
  354. if(!model.get('appropChange')){
  355. $('.main-approp').hide();
  356. } else {
  357. $('.main-approp').show();
  358. }
  359. if(!model.get('expChange')){
  360. $('.main-exp').hide();
  361. } else {
  362. $('.main-exp').show();
  363. }
  364. });
  365. },
  366. updateCrumbs: function(){
  367. var links = ['<a href="/">'+municipalityName+'</a>'];
  368. if(Backbone.history.fragment){
  369. var parts = Backbone.history.fragment;
  370. if (parts.indexOf('?') >= 0){
  371. var idx = parts.indexOf('?');
  372. parts = parts.slice(0,idx).split('/')
  373. } else {
  374. parts = parts.split('/');
  375. }
  376. var crumbs = parts.slice(1, parts.length);
  377. var topView = collection.topLevelView;
  378. var query = {}
  379. $.each(crumbs, function(i, crumb){
  380. var link = '<a href="#' + parts.slice(0,i+2).join('/') + '">';
  381. if(i==0){
  382. var key = topView + ' Slug';
  383. query[key] = crumb;
  384. link += collection.findWhere(query).get(topView);
  385. }
  386. if(i==1){
  387. query['Department Slug'] = crumb;
  388. link += collection.findWhere(query).get('Department');
  389. }
  390. if(i==2){
  391. query['Expense Line Slug'] = crumb;
  392. link += collection.findWhere(query).get('Expense Line');
  393. }
  394. link += '</a>';
  395. links.push(link);
  396. });
  397. }
  398. $('#breadcrumbs').html(links.join(' > '));
  399. },
  400. // This is where the magic happens. Grab the template from the template_cache function
  401. // at the top of this file and then update the chart with what's passed in as the model.
  402. render: function(){
  403. this.$el.html(BudgetHelpers.template_cache('mainChart', {model: this.model}));
  404. this._modelBinder.bind(this.model, this.el, {
  405. viewYear: '.viewYear',
  406. prevYear: '.prevYear',
  407. selectedExp: '.expenditures',
  408. selectedApprop: '.appropriations',
  409. expChange: '.expChange',
  410. appropChange: '.appropChange'
  411. });
  412. this.updateChart(this.model, this.model.get('viewYear'));
  413. return this;
  414. },
  415. updateChart: function(data, year){
  416. if (typeof this.highChart !== 'undefined'){
  417. delete this.highChart;
  418. }
  419. var exps = jQuery.extend(true, [], data.get('expenditures'));
  420. var approps = jQuery.extend(true, [], data.get('appropriations'));
  421. if (debugMode == true) {
  422. console.log('main chart data:')
  423. console.log(exps);
  424. console.log(approps);
  425. }
  426. var exp = [];
  427. var approp = [];
  428. $.each(exps, function(i, e){
  429. if (isNaN(e))
  430. e = null;
  431. else
  432. e = parseInt(e);
  433. exp.push(e);
  434. })
  435. $.each(approps, function(i, e){
  436. if (isNaN(e))
  437. e = null;
  438. else
  439. e = parseInt(e);
  440. approp.push(e);
  441. });
  442. var minValuesArray = $.grep(approp.concat(exp),
  443. function(val) { return val != null; });
  444. var globalOpts = app.GlobalChartOpts;
  445. this.chartOpts.plotOptions.area.pointInterval = globalOpts.pointInterval;
  446. this.chartOpts.plotOptions.area.pointStart = Date.UTC(collection.startYear, 1, 1);
  447. this.chartOpts.plotOptions.series.point.events.click = this.pointClick;
  448. this.chartOpts.series = [{
  449. color: globalOpts.apropColor,
  450. data: approp,
  451. marker: {
  452. radius: 6,
  453. symbol: globalOpts.apropSymbol
  454. },
  455. name: globalOpts.apropTitle
  456. }, {
  457. color: globalOpts.expendColor,
  458. data: exp,
  459. marker: {
  460. radius: 6,
  461. symbol: globalOpts.expendSybmol
  462. },
  463. name: globalOpts.expendTitle
  464. }];
  465. this.chartOpts.yAxis.min = Math.min.apply( Math, minValuesArray )
  466. var selectedYearIndex = year - collection.startYear;
  467. this.highChart = new Highcharts.Chart(this.chartOpts, function(){
  468. this.series[0].data[selectedYearIndex].select(true, true);
  469. this.series[1].data[selectedYearIndex].select(true, true);
  470. });
  471. },
  472. pointClick: function(e){
  473. $("#readme").fadeOut("fast");
  474. $.cookie("budgetbreakdownreadme", "read", { expires: 7 });
  475. var x = this.x,
  476. y = this.y,
  477. selected = !this.selected,
  478. index = this.series.index;
  479. this.select(selected, false);
  480. $.each($('.budget-chart'), function(i, chart){
  481. var sel_points = $(chart).highcharts().getSelectedPoints();
  482. $.each(sel_points, function(i, point){
  483. point.select(false);
  484. });
  485. $.each($(chart).highcharts().series, function(i, serie){
  486. $(serie.data).each(function(j, point){
  487. if(x === point.x && point.y != null) {
  488. point.select(selected, true);
  489. }
  490. });
  491. });
  492. });
  493. var clickedYear = new Date(x).getFullYear();
  494. var yearIndex = this.series.processedYData.indexOf(y);
  495. var hash = window.location.hash;
  496. if(hash.indexOf('?') >= 0){
  497. hash = hash.slice(0, hash.indexOf('?'));
  498. }
  499. app_router.navigate(hash + '?year=' + clickedYear);
  500. collection.updateYear(clickedYear, yearIndex);
  501. $.each($('.bars').children(), function(i, bar){
  502. var width = $(bar).text();
  503. $(bar).css('width', width);
  504. });
  505. },
  506. breakIt: function(e){
  507. e.preventDefault();
  508. var view = $(e.currentTarget).data('choice');
  509. var year = window.location.hash.split('=')[1];
  510. if (year==undefined){
  511. year = activeYear;
  512. }
  513. app_router.navigate('?year=' + year);
  514. collection.updateTables(view, municipalityName, undefined, year);
  515. }
  516. })
  517. // Breakdown Chart view. Does a lot the same kind of things as the main chart view
  518. app.BreakdownSummary = Backbone.View.extend({
  519. tagName: 'tr',
  520. className: 'rowId',
  521. detailShowing: false,
  522. events: {
  523. 'click .details': 'details'
  524. },
  525. initialize: function(){
  526. this._modelBinder = new Backbone.ModelBinder();
  527. var self = this;
  528. this.model.on('change', function(model){
  529. var sel = '#' + model.get('slug') + '-selected-chart';
  530. var exp = accounting.unformat(model.get('expenditures'));
  531. var approp = accounting.unformat(model.get('appropriations'));
  532. if((exp + approp) == 0){
  533. $(self.el).hide();
  534. if($(self.el).next().is(':visible')){
  535. $(self.el).next().hide();
  536. }
  537. } else {
  538. $(self.el).show();
  539. }
  540. if(!model.get('appropChange')){
  541. $(sel).parent().find('.sparkline-budgeted').hide();
  542. } else {
  543. $(sel).parent().find('.sparkline-budgeted').show();
  544. }
  545. if(!model.get('expChange')){
  546. $(sel).parent().find('.sparkline-spent').hide();
  547. } else {
  548. $(sel).parent().find('.sparkline-spent').show();
  549. }
  550. });
  551. },
  552. render: function(){
  553. this.$el.html(BudgetHelpers.template_cache('breakdownSummary', {model:this.model}));
  554. this._modelBinder.bind(this.model, this.el, {
  555. expenditures: {selector: '[name="expenditures"]', converter: this.moneyChanger},
  556. appropriations: {selector: '[name="appropriations"]', converter: this.moneyChanger},
  557. app_perc: {selector: '[name=app_perc]'},
  558. exp_perc: {selector: '[name=exp_perc]'}
  559. });
  560. return this;
  561. },
  562. moneyChanger: function(direction, value){
  563. return accounting.formatMoney(value);
  564. },
  565. details: function(e){
  566. e.preventDefault();
  567. if (typeof this.detailView !== 'undefined'){
  568. this.detailView.undelegateEvents();
  569. }
  570. if (this.$el.next().hasClass('expanded-content')){
  571. this.$el.next().remove();
  572. this.$el.find('img').attr('src', 'images/expand.png')
  573. } else {
  574. var filter = {};
  575. var type = this.model.get('type');
  576. filter[type] = this.model.get('rowName');
  577. var parent_type = this.model.get('parent_type');
  578. if(parent_type){
  579. filter[parent_type] = this.model.get('parent');
  580. }
  581. var expenditures = [];
  582. var appropriations = [];
  583. $.each(collection.getYearRange(), function(i, year){
  584. var exps = collection.where(filter)
  585. var exp = collection.getChartTotals(expendTitle, exps, year);
  586. if (exp.length > 1){
  587. expenditures.push(collection.reduceTotals(exp));
  588. } else {
  589. expenditures.push(parseFloat(exp[0]));
  590. }
  591. var apps = collection.where(filter);
  592. var approp = collection.getChartTotals(apropTitle, apps, year);
  593. if (approp.length > 1){
  594. appropriations.push(collection.reduceTotals(approp));
  595. } else {
  596. appropriations.push(parseFloat(approp[0]));
  597. }
  598. });
  599. this.model.allExpenditures = expenditures;
  600. this.model.allAppropriations = appropriations;
  601. this.detailView = new app.BreakdownDetail({model:this.model});
  602. this.detailView.render().$el.insertAfter(this.$el);
  603. this.detailView.updateChart();
  604. this.$el.find('img').attr('src', 'images/collapse.png')
  605. }
  606. }
  607. })
  608. app.BreakdownDetail = Backbone.View.extend({
  609. tagName: 'tr',
  610. className: 'expanded-content',
  611. chartOpts: window.sparkLineOpts,
  612. events: {
  613. 'click .breakdown': 'breakdownNav'
  614. },
  615. initialize: function(){
  616. this._modelBinder = new Backbone.ModelBinder();
  617. },
  618. render: function(){
  619. this.$el.html(BudgetHelpers.template_cache('breakdownDetail', {model: this.model}));
  620. this._modelBinder.bind(this.model, this.el, {
  621. prevYear: '.prevYear',
  622. expChange: '.expChange',
  623. appropChange: '.appropChange'
  624. });
  625. return this;
  626. },
  627. breakdownNav: function(e){
  628. var filter = {}
  629. var typeView = this.model.get('type');
  630. filter[typeView] = this.model.get('rowName')
  631. var path = this.model.get('slug');
  632. if (this.model.get('parent')){
  633. var hierarchy = collection.hierarchy[collection.topLevelView]
  634. var type_pos = hierarchy.indexOf(typeView)
  635. var parent_type = hierarchy[type_pos - 1];
  636. filter[parent_type] = this.model.get('parent');
  637. path = BudgetHelpers.convertToSlug(this.model.get('parent')) + '/' + this.model.get('slug')
  638. }
  639. collection.updateTables(this.model.get('child'), this.model.get('rowName'), filter, this.model.get('year'));
  640. document.title = document.title + ' | ' + this.model.get('rowName');
  641. $('#secondary-title').text(this.model.get('child'));
  642. var pathStart = null;
  643. if(collection.topLevelView == 'Fund'){
  644. pathStart = 'fund-detail/';
  645. } else {
  646. pathStart = 'control-officer-detail/';
  647. }
  648. $('html, body').animate({
  649. scrollTop: $('#breadcrumbs').offset().top
  650. });
  651. if (debugMode == true) {
  652. console.log('navigating ...')
  653. console.log(pathStart);
  654. console.log(path);
  655. console.log(this.model.get('year'));
  656. }
  657. app_router.navigate(pathStart + path + '?year=' + this.model.get('year'));
  658. collection.mainChartView.updateCrumbs();
  659. },
  660. updateChart: function(){
  661. if (typeof this.highChart !== 'undefined'){
  662. delete this.highChart;
  663. }
  664. var data = this.model;
  665. var exp = [];
  666. var approp = [];
  667. $.each(data.allExpenditures, function(i, e){
  668. if (isNaN(e)){
  669. e = null;
  670. }
  671. exp.push(e);
  672. })
  673. $.each(data.allAppropriations, function(i, e){
  674. if (isNaN(e)){
  675. e = null;
  676. }
  677. approp.push(e);
  678. });
  679. var minValuesArray = $.grep(approp.concat(exp),
  680. function(val) { return val != null; });
  681. if (debugMode == true){
  682. console.log("minValuesArray");
  683. console.log(minValuesArray);
  684. }
  685. var globalOpts = app.GlobalChartOpts;
  686. this.chartOpts.chart.renderTo = data.get('slug') + "-selected-chart";
  687. this.chartOpts.chart.marginBottom = 20;
  688. this.chartOpts.plotOptions.area.pointInterval = globalOpts.pointInterval
  689. this.chartOpts.plotOptions.area.pointStart = Date.UTC(collection.startYear, 1, 1)
  690. this.chartOpts.yAxis.min = Math.min.apply( Math, minValuesArray )
  691. this.chartOpts.plotOptions.series.point.events.click = this.pointClick;
  692. this.chartOpts.series = [{
  693. color: globalOpts.apropColor,
  694. data: approp,
  695. marker: {
  696. radius: 4,
  697. symbol: globalOpts.apropSymbol
  698. },
  699. name: globalOpts.apropTitle
  700. }, {
  701. color: globalOpts.expendColor,
  702. data: exp,
  703. marker: {
  704. radius: 5,
  705. symbol: globalOpts.expendSybmol
  706. },
  707. name: globalOpts.expendTitle
  708. }]
  709. // select current year
  710. var selectedYearIndex = this.model.get('year') - collection.startYear;
  711. this.highChart = new Highcharts.Chart(this.chartOpts, function(){
  712. this.series[0].data[selectedYearIndex].select(true, true);
  713. this.series[1].data[selectedYearIndex].select(true, true);
  714. });
  715. },
  716. // Handler for the click events on the points on the chart
  717. pointClick: function(e){
  718. $("#readme").fadeOut("fast");
  719. $.cookie("budgetbreakdownreadme", "read", { expires: 7 });
  720. var x = this.x,
  721. y = this.y,
  722. selected = !this.selected,
  723. index = this.series.index;
  724. this.select(selected, false);
  725. var active_chart;
  726. $.each($('.budget-chart'), function(i, chart){
  727. var sel_points = $(chart).highcharts().getSelectedPoints();
  728. $.each(sel_points, function(i, point){
  729. point.select(false);
  730. });
  731. $.each($(chart).highcharts().series, function(i, serie){
  732. $(serie.data).each(function(j, point){
  733. if(x === point.x && point.y != null) {
  734. active_chart = chart;
  735. point.select(selected, true);
  736. }
  737. });
  738. });
  739. });
  740. var clickedYear = new Date(x).getFullYear();
  741. var yearIndex = this.series.processedYData.indexOf(y);
  742. var hash = window.location.hash;
  743. if(hash.indexOf('?') >= 0){
  744. hash = hash.slice(0, hash.indexOf('?'));
  745. }
  746. app_router.navigate(hash + '?year=' + clickedYear);
  747. collection.updateYear(clickedYear, yearIndex);
  748. $.each($('.bars').children(), function(i, bar){
  749. var width = $(bar).text();
  750. $(bar).css('width', width);
  751. });
  752. }
  753. });
  754. app.SearchView = Backbone.View.extend({
  755. el: $('#search-form'),
  756. initialize: function(){
  757. var search_options = {
  758. keys: ['Expense Line'],
  759. threshold: 0.4
  760. }
  761. this.Search = new Fuse(collection.toJSON(), search_options);
  762. this.render();
  763. },
  764. events: {
  765. 'click #search': 'engage'
  766. },
  767. render: function(){
  768. this.$el.html(BudgetHelpers.template_cache('search'));
  769. },
  770. engage: function(e){
  771. e.preventDefault();
  772. var input = $(e.currentTarget).parent().prev();
  773. var term = $(input).val();
  774. var results = this.Search.search(term);
  775. if (debugMode == true){
  776. console.log("results");
  777. console.log(results);
  778. }
  779. }
  780. });
  781. app.Router = Backbone.Router.extend({
  782. // Maybe the thing to do here is to construct a separate route for
  783. // the two different top level views. So, fund-detail and control-officer-detail
  784. // or something. That would require making sure the correct route is
  785. // triggered when links are clicked. Not impossible but probably cleaner
  786. routes: {
  787. "fund-detail/:topName(/:secondName)": "fundDetailRoute",
  788. "control-officer-detail/:topName(/:secondName)": "controlDetailRoute",
  789. "(?year=:year)": "defaultRoute"
  790. },
  791. initialize: function(options){
  792. this.collection = options.collection;
  793. },
  794. defaultRoute: function(year){
  795. $('#secondary-title').text('Fund');
  796. var init = undefined;
  797. this.collection.bootstrap(init, year);
  798. },
  799. fundDetailRoute: function(topName, secondName){
  800. var initYear = this.getInitYear('Fund', topName, secondName);
  801. var init = initYear[0];
  802. var year = initYear[1];
  803. this.collection.bootstrap(init, year);
  804. },
  805. controlDetailRoute: function(topName, secondName){
  806. var initYear = this.getInitYear('Control Officer', topName, secondName);
  807. var init = initYear[0];
  808. var year = initYear[1];
  809. this.collection.bootstrap(init, year);
  810. },
  811. getInitYear: function(view, topName, secondName){
  812. var init = [view];
  813. var top = topName;
  814. var idx = topName.indexOf('?');
  815. var year = undefined;
  816. if (idx >= 0){
  817. top = topName.slice(0, idx);
  818. year = topName.slice(idx+1, topName.length).replace('year=', '');
  819. }
  820. init.push(top);
  821. if(secondName){
  822. var second = secondName;
  823. var idx = secondName.indexOf('?');
  824. if (idx >= 0){
  825. second = secondName.slice(0, idx);
  826. year = secondName.slice(idx+1, secondName.length).replace('year=', '');
  827. }
  828. init.push(second);
  829. }
  830. return [init, year]
  831. }
  832. });
  833. var collection = new app.BudgetColl();
  834. var app_router = new app.Router({collection: collection});
  835. Backbone.history.start();
  836. })()