/
explore.js
3393 lines (2740 loc) · 115 KB
/
explore.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
########################################################################################################
#
# PROJECT DSSG: CDPH Lead Exposure Team
#
# AUTHOR Andrew Reece [reece@g.harvard.edu]
#
# FILE /explore.js
#
#
# DESCRIPTION - Provides interactivity and AJAX functionality for explore page (/explore.php)
#
# LIBRARIES D3
# JQuery
# Google Maps
# Spin.js
# This is a Javascript library for 'loading' spinner graphics
# http://fgnass.github.io/spin.js/
#
#
########################################################################################################
*/
/*
Notes:
-This file has gone through a LOT of iterations. I haven't always been good about cleaning up defunct variables,
once they're no longer needed. It may be helpful to just run a search for each of the global variables and see
if they even show up in the actual code, after they're defined. If not, feel free to delete them.
*/
/////// PUBLIC DEMO SETTING
//
// Do you want to show the app as a public (ie. non-confidential) demo?
// (hides addresses and names if set to true)
var public_demo = true;
///////
/////// GLOBAL VARIABLES
///////
var pageview, graphview, old_pview, tabview,
bldg_id, current_building_id, getevents,
current_bounds, current_tool_bounds, rectangle, circles,
map, mapbox_listener, maptool_listener,
referred, spinner, alldata, last_clicked, clickcount,
post_params, current_params, domain_values, search_criteria,
ajax_search_addr, multidata, indivdata, eventsdata, dunit,
svg, svgline, focus, scales, x, y, y_axis, x_axis, g_xax, g_yax,
height, focus_height, focus_width, thresh, dims, radius_scale,
current_addr_full, current_addr_short, current_graph,
rows, head_table, table, dcomplete_PERM, dunit_PERM;
///////
/////// TOGGLE VARIABLES
//////
var toggles, search_crit_popup_toggle, table_exists,
showunit, show_barlevel_data, toggles, showmap,
nohide, noswitch, thresh, active_focus;
// opts are for 'loading status' spinner
// see: http://fgnass.github.io/spin.js/
var opts = {
lines: 13, // The number of lines to draw
length: 20, // The length of each line
width: 10, // The line thickness
radius: 30, // The radius of the inner circle
corners: 1, // Corner roundness (0..1)
rotate: 0, // The rotation offset
direction: 1, // 1: clockwise, -1: counterclockwise
color: '#5287B3', // #rgb or #rrggbb or array of colors
speed: 1, // Rounds per second
trail: 60, // Afterglow percentage
shadow: false, // Whether to render a shadow
hwaccel: false, // Whether to use hardware acceleration
className: 'spinner', // The CSS class to assign to the spinner
zIndex: 2e9, // The z-index (defaults to 2000000000)
top: '80%', // Top position relative to parent
left: '50%' // Left position relative to parent
};
// jquery extension function to extract unique elements from array
// see: http://stackoverflow.com/questions/5381621/jquery-function-to-get-all-unique-elements-from-an-array
$.extend({
distinct : function(anArray) {
var result = [];
$.each(anArray, function(i,v){
if ($.inArray(v, result) == -1) result.push(v);
});
return result;
}
});
// Firefox bug with JQuery .submit() fix:
// http://stackoverflow.com/questions/10108330/firefox-jquery-form-submission-not-working
// all .landing-link <a> tags (Home, City of Chicago icon, and "Lead Inspections Data Portal") use this
// these links load the landing page (/index.php), but we want to pass in any query data that has already been retrieved
// so we turn the objects we want into a JSON string, and pass that as a POST variable in a fake <form> object.
d3.selectAll('.landing-link')
.on('click', function() {
current_params.returncount = post_params.returncount;
var qstring = JSON.stringify({params:current_params,data:multidata});
$.when( $('#container').fadeOut(500) )
.done( function() {
startSpinner('midpage');
setTimeout( function() {
var form = $("<form />")
.attr({ method: "POST", action: "/lead/" })
.append( $('<input />').attr({
type: 'hidden',
name: 'json',
value: '{"postdata":'+qstring+'}'
})
);
$("body").append(form);
form.submit();
}, 500);
});
return false;
});
// global vars that hold AJAX data: multidata, indivdata, eventsdata
// if we have query data from POST, assign to multidata and post_params.
post_params = (postdata !== undefined) ? postdata.params : null;
multidata = (postdata !== undefined) ? postdata.data : null;
// other global ajax vars are null
indivdata = null;
eventsdata = null;
// current_params: gets updated throughout the app with current query parameters
// here it starts with whatever got passed in from POST, or just null
current_params = post_params;
// ajax_search_addr: "value", holds AJAX response from address search query (where matching is done on address string)
ajax_search_addr = null;
// pageview: keeps track of whether we're in 'multi' or 'indiv' views
pageview = (post_params.pview !== undefined) ? post_params.pview : "multi";
// graphview: keeps track of whether we're in "map" or "scatter" (for multi), or "histogram" or "linegraph" (for indiv)
// default starting page for multi is "map", for indiv, "histogram"
graphview = (pageview == "multi") ? "map" : "histogram";
// current_building_id, bldg_id: tracks building id#, a unique number from assessor data for every building in chicago
// used for SQL lookups when we load indiv pages
current_building_id = bldg_id = (post_params.bldg_id !== undefined) ? post_params.bldg_id : "";
// define default y-axis measure
var current_response = "p_high_g5_build";
// old_pview: when transitioning from graph to graph, it's helpful to know whether we're coming from multi or indiv
// old_pview keeps track of this (compared with pview/pageview, which tracks the view we're going to)
old_pview = null;
// tabview: tracks which tab (usually "display", "map", "control") we have visible in the control panel area
tabview = "display-tab";
// getevents: used for AJAX queries for indiv building data.
// we have a separate call for querying events and tests data, i don't remember why.
// more on this in callAjax() documentation.
getevents = "false";
// search_crit_popup_toggle: toggles hide/show of search criteria popup window
search_crit_popup_toggle = false;
// last_clicked: used in indiv view. there are a lot of ways to display information in the table at bottom right.
// depending on whether you click on a histogram bar, a table row, or the 'Show All' link at the bottom,
// there's different behavior that happens with the table display. there were a lot of little bugs with
// this network of click possibilities, so this variable is used to keep track of where the most recent
// table display command came from.
// see: createHistogram(), createTable()
last_clicked = "";
// clickcount: not sure if we still need this one. it was originally used around line 386, where we have an onclick
// trigger for the scatter plot. it has to do with the fact that clicking a multi on the plot sets
// the building id related to that multi, but i wanted to be able to just click anywhere else on the screen
// and have it un-set that building id (rather than having to reclick the multi to unselect it.)
// this used to be more of an issue when the display tab had a histogram view, instead of a table view,
// and i'm not sure if this variable is still necessary.
clickcount = 0;
// get height from css defs in /explore.css
height = d3.select("#vis").style("height").substr(0,3);
// holds threshold booleans for scatter, linegraph
thresh = { "5":false, "10":false, "first-bad":false, "first-insp":false, "comply":false };
// define dimensions for graph formatting
dims = setDims(height);
// boolean for createTable() (should we wipe out existing table elements upon creation?)
table_exists = false;
// boolean for createTable() (should we show table data per-test or per-apt-unit?)
showunit = false;
// boolean for indiv-histogram/table (should we show only records for a given bar on the histogram?)
show_barlevel_data = false;
// for column sorting in indiv. graph (is sorting asc or desc for each column in the table?)
toggles = [];
// for multi page - tells createScatter() whether map view is currently showing (to know whether to hide map div)
showmap = false;
// urls for kml files
// (google maps api doesn't like local filepaths,
// although i guess now that we have a dedicated AWS instance we can use that instead of my own server space)
var kml_urls = {
corridors: 'http://datapsych.com/test/chicago_industrial_corridors.kml',
tracts:'http://datapsych.com/test/chicago_tracts.kmz',
communities:'http://datapsych.com/test/chicago_communities.kml'
};
// for secondary sort key - default -> age_at_sample (indiv.histogram)
var secondary_key = "age_at_sample";
// intro text printed at top of screen (single.b gets replaced dynamically)
var intro_text = {
multi: "Circles represent buildings which match search criteria (<a id='search-criteria-popup-link' href='#'>click to view search criteria</a>)",
single: {a:"Lead exposure report for ", b:"a given building"}
};
// text for #return-count div at bottom right of multi table
var result_count_text = {a:"Query returned ", b:0, c:" records."};
// this is for the search criteria dropdown in #intro
// the idea is to translate variable names into more readable descriptions of each parameter
var criteria_dict = {
keys: {
n_high_tests_build: "Limit var. min.",
n_high_g5_build: "Limit var. min.",
n_high_g10_build: "Limit var. min.",
p_high_g5_build: "Limit var. min.",
p_high_g10_build: "Limit var. min.",
init_date: "Initial inspection date: ",
comply_date: "Compliance date: ",
minlat: "Min. latitude",
maxlat: "Max latitude",
minlon: "Min. longitude",
maxlon: "Max longitude",
qlimit: "# results",
abatement: "Has remediated",
addr_vector: "Address vector",
housebuilt: "Construction era",
housetype: "Residence type",
meas: "Limiting variable",
sample_when: "Blood test dates",
sincelast: "Days since"
},
vals: {
n_high_tests_build: "# tests (total)",
n_high_g5_build: "# tests over BLL 5",
n_high_g10_build: "# tests over BLL 10",
p_high_g5_build: "% of all tests over BLL 5",
p_high_g10_build: "% of all tests over BLL 10",
}
};
// same idea as criteria_dict, but for the indiv-linegraph tooltips
var tooltip_dict = {
event_date: "Date",
event_code: "Event code",
event_text: "Event",
event_comments: "Comments",
age_at_sample_yrs: "Age at test",
apt_num: "Apt #",
bll: "BLL",
fullname: "Name"
};
// toggles for showLayers()
var layers = {
corridors: { kml: null , show: false },
tracts: { kml: null , show: false },
communities: { kml: null , show: false }
};
// on open queries (ie. queries that come from Apply Filter button in Multi control panel with geo-boundaries turned off),
// it's possible that the whole database can get returned, or at least very large chunks of it. query_limit is used to
// set off a warning popup when a large number of hits is returned. set it at any number that seems reasonable. 500 default.
var query_limit = 500;
// current_params keeps getting updated throughout script; these are starting values (mostly from post_params)
current_params.getevents = getevents;
current_params.pview = pageview;
current_params.gview = graphview;
current_params.tview = tabview;
current_params.bldg_id = bldg_id;
current_params.has_initdate = 'false';
current_params.minlat = post_params.minlat;
current_params.maxlat = post_params.maxlat;
current_params.minlon = post_params.minlon;
current_params.maxlon = post_params.maxlon;
// this is how we know whether to re-query DB on loading Multi pages, or whether to just keep current query object
filters_updated = false;
// render page
startload( current_params );
/*
//////////////////////////////////////
//
//
// FUNCTIONS
//
//
//////////////////////////////////////
- most important functions are listed first:
startload()
loadMulti()
createMainMap()
createScatter()
loadIndiv()
createLineGraph()
createHistogram()
createTable()
graph()
getQueryParams()
callAjax()
- the rest are listed in no particular order
*/
//////////////////////////////////////////////////////////////////////////////////////
//
// FUNCTION: startload(params)
// Purpose: initiates pageload, either from landing, after AJAX or Multi/Indiv switch
//
//////////////////////////////////////////////////////////////////////////////////////
function startload(params) {
// set global 'current_params' to whatever current query parameters come into startload()
current_params = params;
// if pview = indiv, initiate indiv page loading sequence
if (params.pview == 'indiv') {
preploadIndiv( params );
// if we've arrived here after clicking Apply Filters in Multi control panel,
// we need to re-query DB, so callAjax()
} else if ((params.pview == "multi") && (filters_updated)) {
startSpinner();
filters_updated = false;
setTimeout( function() { callAjax(params, query_limit); }, 500);
// otherwise, we default to loadMulti() with current query set
} else {
loadMulti(params);
}
}
//////////////////////////////////////////////////////////////////////////////////////
//
// FUNCTION: loadMulti(params)
// Purpose: loads multi view (either map or scatterplot)
//
//////////////////////////////////////////////////////////////////////////////////////
function loadMulti(params) {
// set global 'current_params' to whatever current query parameters are passed in
current_params = params;
// set domains for graph scales
setDomainValues();
// set range, text, etc for graph scales
setScales(domain_values);
// set globals 'graphview' and 'tabview', based on current params
graphview = params.gview;
tabview = params.tview;
// set up tab highlighting on Details pane
changeTab(tabview);
// if svg exists, remove it so we can start over
if(svg !== undefined) {svg.remove();}
// toggles for control panel switches
nohide = false;
noswitch = false;
active_focus = false;
// write #intro text
d3.select("#intro").html(intro_text.multi);
// write #search-criteria-popup text using populateCriteriaBox()
d3.select("#search-criteria-popup").html(function() { return populateCriteriaBox( params ); });
// set click behavior for search criteria popup link (it's actually a dropdown)
d3.select("#search-criteria-popup-link")
.on('click', function() {
search_crit_popup_toggle = (search_crit_popup_toggle) ? false : true;
if (search_crit_popup_toggle) { $('#search-criteria-popup').slideDown(500); }
else { $('#search-criteria-popup').slideUp(500); }
});
// click behavior for 'X' closer on search criteria dropdown
d3.select("#popup-x")
.on('click', function() { search_crit_popup_toggle = false; $('#search-criteria-popup').slideUp(500); });
// make query results string (positioned underneath details box)
result_count_text.b = multidata.length;
d3.select("#return-count")
.style("display", "inline-block")
.html(d3.values(result_count_text).join(""));
// sets text of display tab (this changes between "Building Data" for Multi and "Map View" for Indiv)
setDisplayTabText("multi");
// sets highlight/click behavior for all title tabs on Detail pane
setTabEvents("multi");
// initialize main svg for Multi graph area
svg = d3.select("#vis").append("svg")
.attr('id','multi-box')
.attr({
width: dims.multi.scatter.w,
height: dims.multi.scatter.h
})
.on('click', function() {
clickcount++;
if (noswitch && clickcount > 2) {
nohide = (nohide) ? false : true;
noswitch = (noswitch) ? false : true;
eraseFocus();
clickcount = 0;
}
});
// setParams() automates assigning all the styles for whatever object gets passed in
// it's not really necessary, just de-clutters things a bit
setParams("#tab-bar", dims.multi.tabbar);
setParams("#detail-box", dims.multi.detailbox);
d3.select("#detail-control-group")
.style("top", dims.multi.controlbox.top)
.style("width", dims.multi.controlbox.width);
// this tabname/table_divs thing was a late modification, after I switched out the mini histogram for the table in Multi
// there was some difficulty with getting the right Detail pane tab to show, so this was the hack I used to solve it.
var tabname = tabview.split("-")[0];
var table_divs = (tabname == "display") ? "#table-container, #header-table," : "";
// fade in page elements
$("#container, #vis, #intro, #detail-box, #tab-bar, .multi."+tabname+", .overlay2, "+table_divs+" #return-count").fadeIn(1000);
d3.select('#container').style('opacity', 1.0);
// if spinner is going, stop it
if (spinner !== undefined) { spinner.stop(); }
// if showing #display-tab, generate the table of building data
if (tabname == "display") { createTable(params.pview, multidata); }
// define submit button behavior for search bar
d3.select('#search-submit')
.on('click', function() {
var search_term = d3.select('#search-box').property('value').toUpperCase();
var search_id = null;
var search_data = null;
// if the search box is empty, nothing happens on submit
if (search_term !== "") {
// first try and match address string to a bldg_id in the current query set
multidata.forEach( function(addr) {
if ( addr.address.toUpperCase() === search_term ) {
search_id = addr.bldg_id;
search_data = addr;
}
});
// if that works, then we're done, and we can call transition() (which brings us to Indiv pages)
if (search_id !== null) {
transition( null, search_id, search_data );
// if that doesn't work, query DB and look for building data matching on address string
} else {
callAjax( {getevents:'false', search:'true', addr:search_term} );
search_id = (ajax_search_addr !== null) ? ajax_search_addr[0].bldg_id : null;
// if that works, then we're done and we call transition()
if (search_id !== null) { transition(null, search_id, ajax_search_addr); }
// if we can't find the address in the DB, we give up.
else { alert('Sorry, address not found'); }
}
}
});
////
//// SET EVENT BEHAVIORS FOR CONTROL PANEL
////
// shows different control panel options for map vs scatterplot
changeControlFeatures();
// switches between the four graph views of explore.php (Map, Scatterplot, Histogram, Timeline)
// ** CONSIDER PLACING THIS SOMEWHERE MORE PROMINENT ON PAGE **
d3.selectAll(".graphview")
.on('change', function() {
transition( d3.select(this) );
});
// #filter-mapbox determines whether to limit query to viewable onscreen map area
d3.select('#filter-mapbox')
.on('click', function() {
if (d3.select(this).property('checked')) {
mapbox_listener = google.maps.event.addListener(map, "bounds_changed", function() {
current_bounds = rectangle.getBounds();
bounds_arr = current_bounds.toUrlValue().split(",");
current_params.minlat = bounds_arr[0];
current_params.minlon = bounds_arr[1];
current_params.maxlat = bounds_arr[2];
current_params.maxlon = bounds_arr[3];
});
// if not, we use landing page bounding box as search area
// ** SHOULDN'T IT BE COMPLETELY UNBOUNDED THOUGH?? **
} else {
google.maps.event.removeListener(mapbox_listener);
current_params.minlat = post_params.minlat;
current_params.minlon = post_params.minlon;
current_params.maxlat = post_params.maxlat;
current_params.maxlon = post_params.maxlon;
}
});
// if selected, summons map rectangle, just like on landing page
// bounding box is continuously updated when rectangle is moved
d3.select('#filter-maptool')
.on('click', function() {
var bounds_arr;
if (d3.select(this).property('checked')) {
var center = map.getCenter();
setRectangle(center);
maptool_listener = google.maps.event.addListener(rectangle, "bounds_changed", function() {
current_tool_bounds = rectangle.getBounds();
bounds_arr = current_tool_bounds.toUrlValue().split(",");
current_params.minlat = bounds_arr[0];
current_params.minlon = bounds_arr[1];
current_params.maxlat = bounds_arr[2];
current_params.maxlon = bounds_arr[3];
});
} else {
rectangle.setMap(null);
google.maps.event.removeListener(maptool_listener);
bounds_arr = current_bounds.toUrlValue().split(",");
current_params.minlat = bounds_arr[0];
current_params.minlon = bounds_arr[1];
current_params.maxlat = bounds_arr[2];
current_params.maxlon = bounds_arr[3];
}
});
// changes the y-axis variable (eg. avg bll changes to % bll > 5)
d3.select("#yscale")
.on("change", function() {
current_response = d3.select(this).node().value;
y.domain(scales.multi.y[current_response].domain);
svg.select(".yaxis").transition()
.duration(1000)
.ease("sin-in-out")
.call(y_axis)
.call(updateAxisText,"y",current_response,"multi")
.call(updateMain,current_response);
});
// controls changing the y-transformation (eg. log, sqrt) of scatterplot
// NB: this was more useful when we had hundreds or thousands of points on the graph
// now we generally have fewer than 500, so it might just clutter up the control panel
d3.select("#ytransform")
.on("change", function() {
var transform = d3.select(this).node().value;
if (transform == "linear") {
y = d3.scale.linear().domain(scales.multi.y[current_response].domain).range(scales.multi.y[current_response].range);
} else if (transform == "log") {
y = d3.scale.log().domain(scales.multi.y[current_response].domain).range(scales.multi.y[current_response].range);
} else if (transform == "sqrt") {
y = d3.scale.sqrt().domain(scales.multi.y[current_response].domain).range(scales.multi.y[current_response].range);
}
svg.select(".yaxis").transition()
.duration(1000)
.ease("sin-in-out")
.call(y_axis.scale(y))
.call(updateMain,current_response);
});
// adds/removes KML layers on map (eg. industrial corridors)
d3.select("#map-layer-select")
.on('change', function() {
showLayers( d3.select(this).node().value );
});
// chooses Cutoff Type for filtering
// slider behavior is adjusted with updateMeasureSlider(), based on choice.
d3.select("#filter-bll-measure-category")
.on('change', function() {
var thisid = d3.select(this).node().value;
if (thisid == "p_high_g5_build" || thisid == "p_high_g10_build") { updateMeasureSlider(0.1,1,0.1,0.1); }
else if (thisid == "n_high_g5_build" || thisid == "n_high_g10_build") { updateMeasureSlider(0,100,5,5); }
else if (thisid == "mean_high_bll_build") { updateMeasureSlider(5,20,2,5); }
});
// sets text for min/max sliders values at either end of actual slider
d3.selectAll(".filter-bookend.bookend-min")
.text( function() {
var sliderid = d3.select(this).attr("id").slice(7,-4); //slices off 'filter-' and '-low'
return d3.select("#"+sliderid).property("min");
});
d3.selectAll(".filter-bookend.bookend-max")
.text( function() {
var sliderid = d3.select(this).attr("id").slice(7,-5); //slices off 'filter-' and '-high'
return d3.select("#"+sliderid).property("max");
});
// updates #filter-<sliderid>-current to show current slider value at right of slider
d3.selectAll(".filter-slider")
.on('change', function() {
var textval = d3.select(this).property("value");
var sliderid = d3.select(this).attr("id");
d3.select("#filter-"+sliderid+"-current").text( textval );
});
// click behavior for Apply Filters ( leads to getQueryParams() )
d3.select("#filter-submit")
.on('click', function() {
filters_updated = true;
$('#container').fadeTo(500, 0.1, function() {
startSpinner('midpage');
setTimeout( function() { getQueryParams(); }, 300);
});
});
// hide/show legend-box for scatter plot
d3.selectAll(".show-legend")
.on("change", function() {
var command = d3.select(this).attr("id");
showLegend(command);
});
// draw threshold lines at 5/10 bll, just a visual aid
d3.selectAll(".threshold")
.on("change", function() {
var val = d3.select(this).attr("id");
updateThreshold(val, "scatter", null);
});
/////
///// LOAD MULTI GRAPH
/////
graph( "multi", pageview, graphview, multidata );
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// FUNCTION: createMainMap(addr)
// PURPOSE: makes map for Multi-map view (with google map api)
// Reference: http://bl.ocks.org/mbostock/899711
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////
function createMainMap(addr) {
// default lat/lon centered on Chicago's West Loop district, downtown
var chi_lat = 41.821;
var chi_lon = -87.6613;
var chicago = new google.maps.LatLng(chi_lat, chi_lon);
// create map object
map = new google.maps.Map(d3.select("#map-canvas").node(), {
zoom: 10,
center: chicago,
mapTypeId: google.maps.MapTypeId.ROADMAP,
// if streetview is turned on, it causes problems - hard to get back to normal view with d3 layers
streetViewControl: false
});
// set zoom level and centering of map based on selected building locations
setZoom(map);
// global var 'current_bounds' keeps track of current lat/lon boundaries for query
current_bounds = map.getBounds();
// set global var 'mapbox_listener' to change whenever map is moved/zoomed/panned etc
if (mapbox_listener === undefined) { mapbox_listener = google.maps.event.addListener(map, "bounds_changed", function() { current_bounds = map.getBounds(); }); }
// resets maptool checkbox to 'unchecked'
d3.select('#filter-maptool').property('checked', false);
// resets d3 building overlay (ie. red dots)
if (overlay !== undefined) {overlay.setMap(null);}
// NB: most of the "if blah-blah !== undefined" conditionals are in case we're creating a new map over an old one
var overlay = new google.maps.OverlayView();
// Add the container when the overlay is added to the map
overlay.onAdd = function() {
var layer = d3.select(this.getPanes().overlayMouseTarget).append("div")
.attr("class", "map-box");
// Draw each marker as a separate SVG element.
overlay.draw = function() {
var projection = this.getProjection(),
padding = 8;
var marker = layer.selectAll("svg")
.data(multidata)
.each(transform) // update existing markers
.enter().append("svg")
.each(transform);
// Add a circle.
marker.append("svg:circle")
.attr("r", 4)
.attr("cx", padding)
.attr("cy", padding)
.attr("id", function(d) { return "dot-"+d.bldg_id; })
// mouseover circle turns circle pink, larger
// also highlights related table row
.on("mouseover", function(d,i){
d3.select('[data-rowid=row-'+d.bldg_id+']').classed('zebrafinch', true);
d3.select(this).attr('r', 7);
d3.select(this).style('fill', 'magenta');
})
.on("mouseout", function(d){
d3.select('[data-rowid=row-'+d.bldg_id+']').classed('zebrafinch', false);
d3.select(this).attr('r', 4);
d3.select(this).style('fill', 'red');
})
// click circle enters address of building into search box
// also sets global 'current_building_id'
.on("click", function(d) {
current_building_id = d.bldg_id;
d3.select('#search-box').property('value', function() { return (public_demo) ? "1234 XXXXX ST" : d.address; });
})
// dbl-click calls transition() for Indiv pages
.on("dblclick", function(d) {
transition(null, d.bldg_id);
});
// taken directly from Bostock - converts lat/lon to pixel x/y coords
function transform(d) {
d = new google.maps.LatLng(d.civis_latitude, d.civis_longitude);
d = projection.fromLatLngToDivPixel(d);
return d3.select(this)
.style("left", (d.x - padding) + "px")
.style("top", (d.y - padding) + "px");
}
};
};
// Bind our overlay to the map…
overlay.setMap(map);
// global because createScatter() checks it
showmap = true;
}
//////////////////////////////////////////////////////////////////////////////////////
//
// FUNCTION: createScatter(focus)
// Purpose: creates multi building scatterplot
//
//////////////////////////////////////////////////////////////////////////////////////
function createScatter(data) {
/* Note: most x-axis options (especially 'days since inspection', the only one that works right now)
will exclude some buildings, since not all buildings have the requisite data for that dimension.
for instance, many buildings have never been inspected, so they can't have a value on
'days since inspection'. If there are missing buildings, on loading scatterplot there's a
box that shows up and notes how many are missing and why.
so, this 'excluded' var is a counter that keeps track of how many buildings aren't shown.
*/
var excluded = 0;
data.forEach( function(x) {
if (x.init_date === null && x.comply_date === null) { excluded++; }
});
// if there are any excluded buildings, show warning
if (excluded > 0) {
$.when( $('#container').fadeTo(500, 0.1) )
.done( function() {
d3.select('#scatter-exclude-size').html(excluded);
d3.select('#scatter-total-size').html(data.length);
d3.select('#scatter-exclude-popup').style('display', 'inline-block');
});
}
d3.select('#scatter-exclude-submit')
.on('click', function() {
d3.select('#scatter-exclude-popup').style('display', 'none');
$('#container').fadeTo(100, 1.0);
});
// radius scale for size of building circles (between 4 and 28px)
// radius is based on # tests/bldg
radius_scale = d3.scale.linear()
.domain( [ domain_values.min_n_tests, domain_values.max_n_tests ])
.range([4,28]);
// define x/y scales for scatterplot
// default y variable is % tests > 5 bll
y = d3.scale.linear()
.domain(scales.multi.y[current_response].domain)
.range(scales.multi.y[current_response].range);
// default x variable is # days since inspection of any kind (including compliance date)
x = d3.scale.linear()
.domain(scales.multi.x.all.domain)
.range(scales.multi.x.all.range);
// now define axes based on those scales
x_axis = d3.svg.axis()
.scale(x)
.orient("bottom");
y_axis = d3.svg.axis()
.scale(y)
.ticks(8)
.orient("left");
// set 'group' objects to contain both axis and axis text
// set up placement for axis texts
g_yax = setAxisGroup('yaxis','multi','scatter',svg);
g_yax.call(y_axis);
g_xax = setAxisGroup('xaxis','multi','scatter',svg);
g_xax.call(x_axis);
var yaxis_text = setAxisText('yaxis','multi','scatter',g_yax);
var xaxis_text = setAxisText('xaxis','multi','scatter',g_xax);
// write in axis text (see function below)
updateAxisText("","y",current_response,"multi");
updateAxisText("","x","all","multi");
// if map was previously showing, that means graph elements were hidden...so unhide them
if (showmap) {
g_yax.style('display','inline-block');
g_xax.style('display', 'inline-block');
svg.style('display','inline-block');
showmap = false;
}
// make circles for scatterplot, bound to query data
circles = svg.selectAll(".bldg-circle")
.data(data)
.enter()
.append("circle")
.attr("class", "bldg-circle")
.attr("id", function(d) { return d.bldg_id; })
.attr("data-bldgid", function(d) { return 'circle-'+d.bldg_id; })
.style("stroke-width", "1px")
.style("stroke", "darkgray")
.style("fill", function(d) {
// color by n/s/e/w address label
if (d.dir == "S") { return "#e7298a"; }
else if (d.dir == "N") { return "#7570b3"; }
else if (d.dir == "E") { return "#1b9e77"; }
else if (d.dir == "W") { return "#d95f02"; }
})
// hides circles with no valid x-axis value (ie. never had comply or init date)
.style('visibility', function(d) { return (computeSinceLast(d,"single")) ? 'visible' : 'hidden'; })
// mouseover highlights circle and associated table row
.on("mouseover", function(d,i){
d3.select(this).style('fill-opacity', 1.0);
d3.select('[data-rowid=row-'+d.bldg_id+']').classed('zebrafinch', true);
})
.on("mouseout", function(d){
d3.select(this).style('fill-opacity', 0.4);
d3.select('[data-rowid=row-'+d.bldg_id+']').classed('zebrafinch', false);
})
// click enters address into search box and assigns 'current_building_id'
.on("click", function(d) {
current_building_id = d.bldg_id;
d3.select('#search-box').property('value', function() { return (public_demo) ? "1234 W XXXXX ST" : d.address; });
})
// dbl-click calls transition() to Indiv pages
.on("dblclick", function(d) {
transition(null, d.bldg_id);
})
// define circle parameters
.transition()
.duration(1000)
// radius based on # tests at this building
.attr("r", function(d,i) { return radius_scale(d.n_high_tests_build); })
// x dim based on days since last inspection
.attr("cx", function(d) { return x( computeSinceLast(d,"single") ); })
// y dim based on whatever 'current_response' variable is set to
.attr("cy", function(d) {
var maxh = y(d[current_response]) - radius_scale(d.n_high_tests_build);
var minh = (dims.multi.scatter.h-y(d[current_response])) - radius_scale(d.n_high_tests_build);
if (maxh <= 20) {
return (y(d[current_response]) + 1.2*radius_scale(d.n_high_tests_build));
} else if (minh < 50) {
return (y(d[current_response]) - 1.2*radius_scale(d.n_high_tests_build));
} else {
return y(d[current_response]);
}
})
// circles are slightly opaque (and fill in on mouseover)
.attr("fill-opacity", 0.4);
}
//////////////////////////////////////////////////////////////////////////////////////
//
// FUNCTION: loadIndiv(id)
// Purpose: loads individual-building view (including Histogram and Timeline)
//
//////////////////////////////////////////////////////////////////////////////////////
function loadIndiv() {
// reset secondary sort key to default
secondary_key = "age_at_sample";
// dccomplete and dunit are used to hold data for an entire building, and per-unit information, respectively
// i think these are their global 'permanent' versions, as they can switch around in other functions
dcomplete_PERM = [];
dunit_PERM = [];