├── .gitignore ├── README.md ├── bundle.js ├── cleaned ├── nyc-streets.geo.json ├── nyc-streets.txt ├── stations.csv ├── trips-2017-04-01.csv ├── trips-2017-04-18.csv ├── trips-2017-05-21.csv ├── trips-2017-05-22.csv ├── trips-2017-06-03.csv ├── trips-2017-06-04.csv ├── trips-2017-06-05.csv ├── trips-2017-06-09.csv ├── trips-2017-07-15.csv ├── trips-2017-07-16.csv └── trips-2017-07-17.csv ├── images ├── citibike-trips.png ├── citibike-trips.sketch ├── citibike.mov └── design.png ├── index.html ├── old-code └── main.py ├── package-lock.json ├── package.json ├── scripts ├── clean-citibike-trips.js ├── download-data.sh ├── extract-coordinates-from-geojson.js └── process-trips.sh ├── src ├── check-support.js ├── create-buttons.js ├── create-elapsed-time-view.js ├── create-map-renderer.js ├── create-projection.js ├── create-roaming-camera.js ├── create-state-transitioner.js ├── create-timeline.js ├── create-trip-paths-renderer.js ├── create-trip-points-renderer.js ├── helpers.js ├── index.js ├── map.vert ├── pointer-circle.vert ├── setup-dat-gui.js ├── simple.frag ├── simple.vert ├── trip-path.frag ├── trip-path.vert ├── trip-points.frag ├── trip-points.vert ├── trip-state.frag └── trip-state.vert └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # citibike-trips 2 | 3 | ![citibike-trips](/images/citibike-trips.png?raw=true "citibike-trips") 4 | 5 | ``` 6 | npm run serve 7 | ``` 8 | 9 | For dev: 10 | ``` 11 | npm install 12 | npm start 13 | ``` 14 | 15 | Playing with some citibike trip data made available at [https://www.citibikenyc.com/system-data](https://www.citibikenyc.com/system-data). 16 | 17 | Most of the dataset was too large for the repo, so you'll have to go download the data for yourself. 18 | -------------------------------------------------------------------------------- /cleaned/stations.csv: -------------------------------------------------------------------------------- 1 | station_id,latitude,longitude,name 2 | 72,40.76727216,-73.99392888,"W 52 St & 11 Ave" 3 | 79,40.71911552,-74.00666661,"Franklin St & W Broadway" 4 | 82,40.71117416,-74.00016545,"St James Pl & Pearl St" 5 | 83,40.68382604,-73.97632328,"Atlantic Ave & Fort Greene Pl" 6 | 116,40.74177603,-74.00149746,"W 17 St & 8 Ave" 7 | 119,40.69608941,-73.97803415,"Park Ave & St Edwards St" 8 | 120,40.68676793,-73.95928168,"Lexington Ave & Classon Ave" 9 | 127,40.73172428,-74.00674436,"Barrow St & Hudson St" 10 | 128,40.72710258,-74.00297088,"MacDougal St & Prince St" 11 | 143,40.69239502,-73.99337909,"Clinton St & Joralemon St" 12 | 144,40.69839895,-73.98068914,"Nassau St & Navy St" 13 | 146,40.71625008,-74.0091059,"Hudson St & Reade St" 14 | 150,40.7208736,-73.98085795,"E 2 St & Avenue C" 15 | 151,40.722103786686034,-73.99724900722504,"Cleveland Pl & Spring St" 16 | 152,40.71473993,-74.00910627,"Warren St & Church St" 17 | 153,40.752062307,-73.9816324043,"E 40 St & 5 Ave" 18 | 157,40.69089272,-73.99612349,"Henry St & Atlantic Ave" 19 | 161,40.72917025,-73.99810231,"LaGuardia Pl & W 3 St" 20 | 164,40.75323098,-73.97032517,"E 47 St & 2 Ave" 21 | 167,40.7489006,-73.97604882,"E 39 St & 3 Ave" 22 | 168,40.73971301,-73.99456405,"W 18 St & 6 Ave" 23 | 173,40.76068327096592,-73.9845272898674,"Broadway & W 49 St" 24 | 174,40.7381765,-73.97738662,"E 25 St & 1 Ave" 25 | 195,40.70905623,-74.01043382,"Liberty St & Broadway" 26 | 212,40.74334935,-74.00681753,"W 16 St & The High Line" 27 | 216,40.70037867,-73.99548059,"Columbia Heights & Cranberry St" 28 | 217,40.70277159,-73.99383605,"Old Fulton St" 29 | 223,40.73781509,-73.99994661,"W 13 St & 7 Ave" 30 | 228,40.7546011026,-73.971878855,"E 48 St & 3 Ave" 31 | 229,40.72743423,-73.99379025,"Great Jones St" 32 | 232,40.69597683,-73.99014892,"Cadman Plaza E & Tillary St" 33 | 236,40.7284186,-73.98713956,"St Marks Pl & 2 Ave" 34 | 237,40.73047309,-73.98672378,"E 11 St & 2 Ave" 35 | 238,40.7361967,-74.00859207,"Bank St & Washington St" 36 | 239,40.69196566,-73.9813018,"Willoughby St & Fleet St" 37 | 241,40.68981035,-73.97493121,"DeKalb Ave & S Portland Ave" 38 | 242,40.697787,-73.973736,"Carlton Ave & Flushing Ave" 39 | 243,40.688226,-73.979382,"Fulton St & Rockwell Pl" 40 | 244,40.69196035,-73.96536851,"Willoughby Ave & Hall St" 41 | 245,40.69327018,-73.97703874,"Myrtle Ave & St Edwards St" 42 | 247,40.73535398,-74.00483091,"Perry St & Bleecker St" 43 | 248,40.72185379,-74.00771779,"Laight St & Hudson St" 44 | 249,40.71870987,-74.0090009,"Harrison St & Hudson St" 45 | 251,40.72317958,-73.99480012,"Mott St & Prince St" 46 | 252,40.73226398,-73.99852205,"MacDougal St & Washington Sq" 47 | 253,40.73543934,-73.99453948,"W 13 St & 5 Ave" 48 | 254,40.73532427,-73.99800419,"W 11 St & 6 Ave" 49 | 257,40.71939226,-74.00247214,"Lispenard St & Broadway" 50 | 258,40.68940747,-73.96885458,"DeKalb Ave & Vanderbilt Ave" 51 | 259,40.70122128,-74.01234218,"South St & Whitehall St" 52 | 260,40.70365182,-74.01167797,"Broad St & Bridge St" 53 | 261,40.69474881,-73.98362464,"Johnson St & Gold St" 54 | 262,40.6917823,-73.9737299,"Washington Park" 55 | 264,40.70706456,-74.00731853,"Maiden Ln & Pearl St" 56 | 265,40.72229346,-73.99147535,"Stanton St & Chrystie St" 57 | 266,40.72368361,-73.97574813,"Avenue D & E 8 St" 58 | 267,40.75097711,-73.98765428,"Broadway & W 36 St" 59 | 268,40.71910537,-73.99973337,"Howard St & Centre St" 60 | 270,40.69308257,-73.97178913,"Adelphi St & Myrtle Ave" 61 | 274,40.68691865,-73.976682,"Lafayette Ave & Fort Greene Pl" 62 | 275,40.68650065,-73.96563307,"Washington Ave & Greene Ave" 63 | 276,40.71748752,-74.0104554,"Duane St & Greenwich St" 64 | 278,40.69766564,-73.98476437,"Concord St & Bridge St" 65 | 279,40.707873,-74.00167,"Peck Slip & Front St" 66 | 280,40.73331967,-73.99510132,"E 10 St & 5 Ave" 67 | 281,40.7643971,-73.97371465,"Grand Army Plaza & Central Park S" 68 | 282,40.707644944175705,-73.96841526031494,"Kent Ave & S 11 St" 69 | 284,40.7390169121,-74.0026376103,"Greenwich Ave & 8 Ave" 70 | 285,40.73454567,-73.99074142,"Broadway & E 14 St" 71 | 289,40.6845683,-73.95881081,"Monroe St & Classon Ave" 72 | 291,40.713126,-73.984844,"Madison St & Montgomery St" 73 | 293,40.73020660529954,-73.99102628231049,"Lafayette St & E 8 St" 74 | 295,40.71406667,-73.99293911,"Pike St & E Broadway" 75 | 296,40.71413089,-73.9970468,"Division St & Bowery" 76 | 297,40.734232,-73.986923,"E 15 St & 3 Ave" 77 | 301,40.72217444,-73.98368779,"E 2 St & Avenue B" 78 | 302,40.72082834,-73.97793172,"Avenue D & E 3 St" 79 | 303,40.72362738,-73.99949601,"Mercer St & Spring St" 80 | 304,40.70463334,-74.01361706,"Broadway & Battery Pl" 81 | 305,40.76095756,-73.96724467,"E 58 St & 3 Ave" 82 | 306,40.70823502,-74.00530063,"Cliff St & Fulton St" 83 | 308,40.71307916,-73.99851193,"St James Pl & Oliver St" 84 | 309,40.7149787,-74.013012,"Murray St & West St" 85 | 310,40.68926942,-73.98912867,"State St & Smith St" 86 | 311,40.7172274,-73.98802084,"Norfolk St & Broome St" 87 | 312,40.722055,-73.989111,"Allen St & Stanton St" 88 | 313,40.69610226,-73.96751037,"Washington Ave & Park Ave" 89 | 314,40.69383,-73.990539,"Cadman Plaza West & Montague St" 90 | 315,40.70355377,-74.00670227,"South St & Gouverneur Ln" 91 | 316,40.70955958,-74.00653609,"Fulton St & William St" 92 | 317,40.72453734,-73.98185424,"E 6 St & Avenue B" 93 | 319,40.711066,-74.009447,"Fulton St & Broadway" 94 | 320,40.717571,-74.005549,"Leonard St & Church St" 95 | 321,40.69991755,-73.98971773,"Cadman Plaza E & Red Cross Pl" 96 | 322,40.696192,-73.991218,"Clinton St & Tillary St" 97 | 323,40.69236178,-73.98631746,"Lawrence St & Willoughby St" 98 | 324,40.689888,-73.981013,"DeKalb Ave & Hudson Ave" 99 | 325,40.73624527,-73.98473765,"E 19 St & 3 Ave" 100 | 326,40.72953837,-73.98426726,"E 11 St & 1 Ave" 101 | 327,40.7153379,-74.01658354,"Vesey Pl & River Terrace" 102 | 328,40.72405549,-74.00965965,"Watts St & Greenwich St" 103 | 330,40.71450451,-74.00562789,"Reade St & Broadway" 104 | 331,40.71173107,-73.99193043,"Pike St & Monroe St" 105 | 332,40.71219906,-73.97948148,"Cherry St" 106 | 334,40.74238787,-73.99726235,"W 20 St & 7 Ave" 107 | 335,40.72903917,-73.99404649,"Washington Pl & Broadway" 108 | 336,40.73047747,-73.99906065,"Sullivan St & Washington Sq" 109 | 337,40.7037992,-74.00838676,"Old Slip & Front St" 110 | 339,40.72580614,-73.97422494,"Avenue D & E 12 St" 111 | 341,40.71782143,-73.97628939,"Stanton St & Mangin St" 112 | 342,40.71739973,-73.98016555,"Columbia St & Rivington St" 113 | 343,40.69794,-73.96986848,"Clinton Ave & Flushing Ave" 114 | 344,40.6851443,-73.95380904,"Monroe St & Bedford Ave" 115 | 345,40.73649403,-73.99704374,"W 13 St & 6 Ave" 116 | 346,40.73652889,-74.00618026,"Bank St & Hudson St" 117 | 347,40.728846,-74.008591,"Greenwich St & W Houston St" 118 | 348,40.72490985,-74.00154702,"W Broadway & Spring St" 119 | 349,40.71850211,-73.98329859,"Rivington St & Ridge St" 120 | 350,40.71559509,-73.9870295,"Clinton St & Grand St" 121 | 351,40.70530954,-74.00612572,"Front St & Maiden Ln" 122 | 353,40.68539567,-73.97431458,"S Portland Ave & Hanson Pl" 123 | 354,40.69363137,-73.96223558,"Emerson Pl & Myrtle Ave" 124 | 355,40.71602118,-73.99974372,"Bayard St & Baxter St" 125 | 356,40.71622644,-73.98261206,"Bialystoker Pl & Delancey St" 126 | 357,40.73261787,-73.99158043,"E 11 St & Broadway" 127 | 358,40.73291553,-74.00711384,"Christopher St & Greenwich St" 128 | 359,40.75510267,-73.97498696,"E 47 St & Park Ave" 129 | 360,40.70717936,-74.00887308,"William St & Pine St" 130 | 361,40.71605866,-73.99190759,"Allen St & Hester St" 131 | 363,40.70834698,-74.01713445,"West Thames St" 132 | 364,40.68900443,-73.96023854,"Lafayette Ave & Classon Ave" 133 | 365,40.68223166,-73.9614583,"Fulton St & Grand Ave" 134 | 366,40.693261,-73.968896,"Clinton Ave & Myrtle Ave" 135 | 368,40.73038599,-74.00214988,"Carmine St & 6 Ave" 136 | 369,40.73224119,-74.00026394,"Washington Pl & 6 Ave" 137 | 372,40.694528,-73.958089,"Franklin Ave & Myrtle Ave" 138 | 373,40.69331716,-73.95381995,"Willoughby Ave & Walworth St" 139 | 376,40.70862144,-74.00722156,"John St & William St" 140 | 377,40.72243797,-74.00566443,"6 Ave & Canal St" 141 | 379,40.749156,-73.9916,"W 31 St & 7 Ave" 142 | 380,40.73401143,-74.00293877,"W 4 St & 7 Ave S" 143 | 382,40.73492695,-73.99200509,"University Pl & E 14 St" 144 | 383,40.735238,-74.000271,"Greenwich Ave & Charles St" 145 | 384,40.683048,-73.964915,"Fulton St & Washington Ave" 146 | 385,40.75797322,-73.96603308,"E 55 St & 2 Ave" 147 | 386,40.71494807,-74.00234482,"Centre St & Worth St" 148 | 387,40.71273266,-74.0046073,"Centre St & Chambers St" 149 | 388,40.749717753,-74.002950346,"W 26 St & 10 Ave" 150 | 389,40.71044554,-73.96525063,"Broadway & Berry St" 151 | 390,40.69221589,-73.9842844,"Duffield St & Willoughby St" 152 | 391,40.69760127,-73.99344559,"Clark St & Henry St" 153 | 392,40.695065,-73.987167,"Jay St & Tech Pl" 154 | 393,40.72299208,-73.97995466,"E 5 St & Avenue C" 155 | 394,40.72521311,-73.97768752,"E 9 St & Avenue C" 156 | 395,40.68807003,-73.98410637,"Bond St & Schermerhorn St" 157 | 396,40.680342423,-73.9557689392,"Lefferts Pl & Franklin Ave" 158 | 397,40.68415748,-73.96922273,"Fulton St & Clermont Ave" 159 | 398,40.69165183,-73.9999786,"Atlantic Ave & Furman St" 160 | 399,40.68851534,-73.9647628,"Lafayette Ave & St James Pl" 161 | 400,40.71926081,-73.98178024,"Pitt St & Stanton St" 162 | 401,40.72019576,-73.98997825,"Allen St & Rivington St" 163 | 402,40.7403432,-73.98955109,"Broadway & E 22 St" 164 | 403,40.72502876,-73.99069656,"E 2 St & 2 Ave" 165 | 405,40.739323,-74.008119,"Washington St & Gansevoort St" 166 | 406,40.69512845,-73.99595065,"Hicks St & Montague St" 167 | 407,40.700469,-73.991454,"Henry St & Poplar St" 168 | 408,40.71076228,-73.99400398,"Market St & Cherry St" 169 | 409,40.6906495,-73.95643107,"DeKalb Ave & Skillman St" 170 | 410,40.72066442,-73.98517977,"Suffolk St & Stanton St" 171 | 411,40.72228087,-73.97668709,"E 6 St & Avenue D" 172 | 412,40.7158155,-73.99422366,"Forsyth St & Canal St" 173 | 414,40.70281858,-73.98765762,"Pearl St & Anchorage Pl" 174 | 415,40.7047177,-74.00926027,"Pearl St & Hanover Square" 175 | 416,40.68753406,-73.97265183,"Cumberland St & Lafayette Ave" 176 | 417,40.71291224,-74.01020234,"Barclay St & Church St" 177 | 418,40.70224,-73.982578,"Front St & Gold St" 178 | 419,40.69580705,-73.97355569,"Carlton Ave & Park Ave" 179 | 420,40.68764484,-73.96968902,"Clermont Ave & Lafayette Ave" 180 | 421,40.69573398,-73.97129668,"Clermont Ave & Park Ave" 181 | 422,40.770513,-73.988038,"W 59 St & 10 Ave" 182 | 423,40.76584941,-73.98690506,"W 54 St & 9 Ave" 183 | 426,40.71754834,-74.01322069,"West St & Chambers St" 184 | 427,40.701907,-74.013942,"Bus Slip & State St" 185 | 428,40.72467721,-73.98783413,"E 3 St & 1 Ave" 186 | 430,40.7014851,-73.98656928,"York St & Jay St" 187 | 432,40.72621788,-73.98379855,"E 7 St & Avenue A" 188 | 433,40.72955361,-73.98057249,"E 13 St & Avenue A" 189 | 434,40.74317449,-74.00366443,"9 Ave & W 18 St" 190 | 435,40.74173969,-73.99415556,"W 21 St & 6 Ave" 191 | 436,40.68216564,-73.95399026,"Hancock St & Bedford Ave" 192 | 437,40.6809833854,-73.9500479759,"Macon St & Nostrand Ave" 193 | 438,40.72779126,-73.98564945,"St Marks Pl & 1 Ave" 194 | 439,40.7262807,-73.98978041,"E 4 St & 2 Ave" 195 | 440,40.75255434,-73.97282625,"E 45 St & 3 Ave" 196 | 441,40.756014,-73.967416,"E 52 St & 2 Ave" 197 | 442,40.746647,-73.993915,"W 27 St & 7 Ave" 198 | 443,40.70853074,-73.96408963,"Bedford Ave & S 9 St" 199 | 444,40.7423543,-73.98915076,"Broadway & W 24 St" 200 | 445,40.72740794,-73.98142006,"E 10 St & Avenue A" 201 | 446,40.74487634,-73.99529885,"W 24 St & 7 Ave" 202 | 447,40.76370739,-73.9851615,"8 Ave & W 52 St" 203 | 448,40.75660359,-73.9979009,"W 37 St & 10 Ave" 204 | 449,40.76461837,-73.98789473,"W 52 St & 9 Ave" 205 | 450,40.76227205,-73.98788205,"W 49 St & 8 Ave" 206 | 453,40.74475148,-73.99915362,"W 22 St & 8 Ave" 207 | 454,40.75455731,-73.96592976,"E 51 St & 1 Ave" 208 | 455,40.75001986,-73.96905301,"1 Ave & E 44 St" 209 | 456,40.7597108,-73.97402311,"E 53 St & Madison Ave" 210 | 457,40.76695317,-73.98169333,"Broadway & W 58 St" 211 | 458,40.751396,-74.005226,"11 Ave & W 27 St" 212 | 459,40.746745,-74.007756,"W 20 St & 11 Ave" 213 | 460,40.71285887,-73.96590294,"S 4 St & Wythe Ave" 214 | 461,40.73587678,-73.98205027,"E 20 St & 2 Ave" 215 | 462,40.74691959,-74.00451887,"W 22 St & 10 Ave" 216 | 465,40.75513557,-73.98658032,"Broadway & W 41 St" 217 | 466,40.74395411,-73.99144871,"W 25 St & 6 Ave" 218 | 467,40.68312489,-73.97895137,"Dean St & 4 Ave" 219 | 468,40.7652654,-73.98192338,"Broadway & W 56 St" 220 | 469,40.76344058,-73.98268129,"Broadway & W 53 St" 221 | 470,40.74345335,-74.00004031,"W 20 St & 8 Ave" 222 | 471,40.71286844,-73.95698119,"Grand St & Havemeyer St" 223 | 472,40.7457121,-73.98194829,"E 32 St & Park Ave" 224 | 473,40.72110063,-73.9919254,"Rivington St & Chrystie St" 225 | 474,40.7451677,-73.98683077,"5 Ave & E 29 St" 226 | 476,40.74394314,-73.97966069,"E 31 St & 3 Ave" 227 | 477,40.75640548,-73.9900262,"W 41 St & 8 Ave" 228 | 478,40.76030096,-73.99884222,"11 Ave & W 41 St" 229 | 479,40.76019252,-73.9912551,"9 Ave & W 45 St" 230 | 480,40.76669671,-73.99061728,"W 53 St & 10 Ave" 231 | 481,40.71260486,-73.96264403,"S 3 St & Bedford Ave" 232 | 482,40.73935542,-73.99931783,"W 15 St & 7 Ave" 233 | 483,40.73223272,-73.98889957,"E 12 St & 3 Ave" 234 | 484,40.75500254,-73.98014437,"W 44 St & 5 Ave" 235 | 485,40.75038009,-73.98338988,"W 37 St & 5 Ave" 236 | 486,40.7462009,-73.98855723,"Broadway & W 29 St" 237 | 487,40.73314259,-73.97573881,"E 20 St & FDR Drive" 238 | 490,40.751551,-73.993934,"8 Ave & W 33 St" 239 | 491,40.74096374,-73.98602213,"E 24 St & Park Ave S" 240 | 492,40.75019995,-73.99093085,"W 33 St & 7 Ave" 241 | 494,40.74734825,-73.99723551,"W 26 St & 8 Ave" 242 | 495,40.76269882,-73.99301222,"W 47 St & 10 Ave" 243 | 496,40.73726186,-73.99238967,"E 16 St & 5 Ave" 244 | 497,40.73704984,-73.99009296,"E 17 St & Broadway" 245 | 498,40.74854862,-73.98808416,"Broadway & W 32 St" 246 | 499,40.76915505,-73.98191841,"Broadway & W 60 St" 247 | 500,40.76228826,-73.98336183,"Broadway & W 51 St" 248 | 501,40.744219,-73.97121214,"FDR Drive & E 35 St" 249 | 502,40.714215,-73.981346,"Henry St & Grand St" 250 | 503,40.73827428,-73.98751968,"E 20 St & Park Ave" 251 | 504,40.73221853,-73.98165557,"1 Ave & E 16 St" 252 | 505,40.74901271,-73.98848395,"6 Ave & W 33 St" 253 | 507,40.73912601,-73.97973776,"E 25 St & 2 Ave" 254 | 508,40.76341379,-73.99667444,"W 46 St & 11 Ave" 255 | 509,40.7454973,-74.00197139,"9 Ave & W 22 St" 256 | 513,40.768254,-73.988639,"W 56 St & 10 Ave" 257 | 514,40.76087502,-74.00277668,"12 Ave & W 40 St" 258 | 515,40.76009437,-73.99461843,"W 43 St & 10 Ave" 259 | 516,40.75206862,-73.96784384,"E 47 St & 1 Ave" 260 | 517,40.751581,-73.97791,"Pershing Square South" 261 | 518,40.74780373,-73.9734419,"E 39 St & 2 Ave" 262 | 519,40.751873,-73.977706,"Pershing Square North" 263 | 520,40.75992262,-73.97648516,"W 52 St & 5 Ave" 264 | 522,40.75714758,-73.97207836,"E 51 St & Lexington Ave" 265 | 523,40.75466591,-73.99138152,"W 38 St & 8 Ave" 266 | 524,40.75527307,-73.98316936,"W 43 St & 6 Ave" 267 | 525,40.75594159,-74.0021163,"W 34 St & 11 Ave" 268 | 526,40.74765947,-73.98490707,"E 33 St & 5 Ave" 269 | 527,40.744023,-73.976056,"E 33 St & 2 Ave" 270 | 528,40.74290902,-73.97706058,"2 Ave & E 31 St" 271 | 529,40.7575699,-73.99098507,"W 42 St & 8 Ave" 272 | 530,40.771522,-73.990541,"11 Ave & W 59 St" 273 | 531,40.71893904,-73.99266288,"Forsyth St & Broome St" 274 | 532,40.710451,-73.960876,"S 5 Pl & S 4 St" 275 | 534,40.70255065,-74.0127234,"Water - Whitehall Plaza" 276 | 536,40.74144387,-73.97536082,"1 Ave & E 30 St" 277 | 537,40.74025878,-73.98409214,"Lexington Ave & E 24 St" 278 | 539,40.71534825,-73.96024116,"Metropolitan Ave & Bedford Ave" 279 | 540,40.74311555376486,-73.98215353488922,"Lexington Ave & E 29 St" 280 | 545,40.736502,-73.97809472,"E 23 St & 1 Ave" 281 | 546,40.74444921,-73.98303529,"E 30 St & Park Ave S" 282 | 2000,40.70255088,-73.98940236,"Front St & Washington St" 283 | 2001,40.699773,-73.979927,"Sands St & Navy St" 284 | 2002,40.716887,-73.963198,"Wythe Ave & Metropolitan Ave" 285 | 2003,40.733812191966315,-73.98054420948029,"1 Ave & E 18 St" 286 | 2005,40.70531194,-73.97100056,"Railroad Ave & Kay Ave" 287 | 2006,40.76590936,-73.97634151,"Central Park S & 6 Ave" 288 | 2008,40.70569254,-74.01677685,"Little West St & 1 Pl" 289 | 2009,40.71117444,-73.99682619,"Catherine St & Monroe St" 290 | 2010,40.72165481,-74.00234737,"Grand St & Greene St" 291 | 2012,40.739445,-73.976806,"E 27 St & 1 Ave" 292 | 2021,40.75929124,-73.98859651,"W 45 St & 8 Ave" 293 | 2022,40.759107,-73.959223,"E 60 St & York Ave" 294 | 2023,40.75968085,-73.97031366,"E 55 St & Lexington Ave" 295 | 3002,40.711512,-74.015756,"South End Ave & Liberty St" 296 | 3016,40.72036775298455,-73.96165072917938,"Kent Ave & N 7 St" 297 | 3041,40.67890679,-73.94142771,"Kingston Ave & Herkimer St" 298 | 3042,40.6794268,-73.9298911,"Fulton St & Utica Ave" 299 | 3043,40.6814598,-73.934903,"Lewis Ave & Decatur St" 300 | 3044,40.6800105,-73.938475,"Albany Ave & Fulton St" 301 | 3046,40.682601,-73.938037,"Marcus Garvey Blvd & Macon St" 302 | 3047,40.6823687,-73.944118,"Halsey St & Tompkins Ave" 303 | 3048,40.68402,-73.94977,"Putnam Ave & Nostrand Ave" 304 | 3049,40.68488,-73.96304,"Cambridge Pl & Gates Ave" 305 | 3050,40.6851532,-73.94111,"Putnam Ave & Throop Ave" 306 | 3052,40.686312,-73.935775,"Lewis Ave & Madison St" 307 | 3053,40.6900815,-73.947915,"Marcy Ave & Lafayette Ave" 308 | 3054,40.6894932,-73.942061,"Greene Ave & Throop Ave" 309 | 3055,40.6883337,-73.950916,"Greene Ave & Nostrand Ave" 310 | 3056,40.69072549,-73.95133465,"Kosciuszko St & Nostrand Ave" 311 | 3057,40.69128258,-73.9452416,"Kosciuszko St & Tompkins Ave" 312 | 3058,40.69237074,-73.93705428,"Lewis Ave & Kosciuszko St" 313 | 3059,40.6933982,-73.939877,"Pulaski St & Marcus Garvey Blvd" 314 | 3060,40.69425403,-73.94626915,"Willoughby Ave & Tompkins Ave" 315 | 3061,40.69622937,-73.94371094,"Throop Ave & Myrtle Ave" 316 | 3062,40.69539817,-73.94954908,"Myrtle Ave & Marcy Ave" 317 | 3063,40.69527008,-73.95238108,"Nostrand Ave & Myrtle Ave" 318 | 3064,40.69681963,-73.93756926,"Myrtle Ave & Lewis Ave" 319 | 3065,40.70029511,-73.95032283,"Union Ave & Wallabout St" 320 | 3066,40.69957608,-73.94708417,"Tompkins Ave & Hopkins St" 321 | 3067,40.7016657,-73.9437303,"Broadway & Whipple St" 322 | 3068,40.7031724,-73.940636,"Humboldt St & Varet St" 323 | 3069,40.70411791,-73.94818595,"Lorimer St & Broadway" 324 | 3070,40.70510918,-73.94407279,"McKibbin St & Manhattan Ave" 325 | 3071,40.70538077,-73.94976519,"Boerum St & Broadway" 326 | 3072,40.70583339,-73.94644578,"Leonard St & Boerum St" 327 | 3073,40.70691254,-73.95441667,"Division Ave & Hooper St" 328 | 3074,40.70767788,-73.94016171,"Montrose Ave & Bushwick Ave" 329 | 3075,40.70708701,-73.95796783,"Division Ave & Marcy Ave" 330 | 3076,40.70870368,-73.9448625,"Scholes St & Manhattan Ave" 331 | 3077,40.70877084,-73.95095259,"Stagg St & Union Ave" 332 | 3078,40.70924826276157,-73.96063148975372,"Broadway & Roebling St" 333 | 3080,40.70934,-73.95608,"S 4 St & Rodney St" 334 | 3081,40.711863,-73.944024,"Graham Ave & Grand St" 335 | 3082,40.71167351,-73.95141312,"Hope St & Union Ave" 336 | 3083,40.71247661,-73.94100005,"Bushwick Ave & Powers St" 337 | 3085,40.71469,-73.95739,"Roebling St & N 4 St" 338 | 3086,40.715143,-73.944507,"Graham Ave & Conselyea St" 339 | 3087,40.71413311,-73.95234386,"Metropolitan Ave & Meeker Ave" 340 | 3088,40.7160751,-73.952029,"Union Ave & Jackson St" 341 | 3090,40.71774592,-73.95600096,"N 8 St & Driggs Ave" 342 | 3091,40.71764,-73.94882,"Frost St & Meeker St" 343 | 3092,40.7190095,-73.95852515,"Berry St & N 8 St" 344 | 3093,40.71745169,-73.95850939,"N 6 St & Bedford Ave" 345 | 3094,40.7169811,-73.94485918,"Graham Ave & Withers St" 346 | 3095,40.71929301,-73.94500379,"Graham Ave & Herbert St" 347 | 3096,40.71924,-73.95242,"Union Ave & N 12 St" 348 | 3100,40.724812564400175,-73.94752621650696,"Nassau Ave & Newell St" 349 | 3101,40.72079821,-73.95484712,"N 12 St & Bedford Ave" 350 | 3102,40.72179134,-73.9504154,"Driggs Ave & Lorimer St" 351 | 3103,40.72153267,-73.95782357,"N 11 St & Wythe Ave" 352 | 3105,40.724055,-73.955736,"N 15 St & Wythe Ave" 353 | 3106,40.72325,-73.94308,"Driggs Ave & N Henry St" 354 | 3107,40.72311651,-73.95212324,"Bedford Ave & Nassau Ave" 355 | 3108,40.72557,-73.94434,"Nassau Ave & Russell St" 356 | 3109,40.72606,-73.95621,"Banker St & Meserole Ave" 357 | 3110,40.72708584,-73.95299117,"Meserole Ave & Manhattan Ave" 358 | 3112,40.72906,-73.95779,"Milton St & Franklin St" 359 | 3113,40.73026,-73.95394,"Greenpoint Ave & Manhattan Ave" 360 | 3115,40.73232194,-73.9550858,"India St & Manhattan Ave" 361 | 3116,40.73266,-73.95826,"Huron St & Franklin St" 362 | 3117,40.73564,-73.95866,"Franklin St & Dupont St" 363 | 3118,40.73555,-73.95284,"McGuinness Blvd & Eagle St" 364 | 3119,40.74232744,-73.95411749,"Vernon Blvd & 50 Ave" 365 | 3120,40.74161,-73.96044,"Center Blvd & Borden Ave" 366 | 3121,40.74524768,-73.94733276,"Jackson Ave & 46 Rd" 367 | 3122,40.744363287066875,-73.9558732509613,"48 Ave & 5 St" 368 | 3123,40.74469738,-73.93540375,"31 St & Thomson Ave" 369 | 3124,40.74731,-73.95451,"46 Ave & 5 St" 370 | 3125,40.74708586,-73.94977234,"45 Rd & 11 St" 371 | 3126,40.74718234,-73.9432635,"44 Dr & Jackson Ave" 372 | 3127,40.74966,-73.9521,"9 St & 44 Rd" 373 | 3128,40.75052534,-73.94594845,"21 St & 43 Ave" 374 | 3129,40.75110165,-73.94073717,"Queens Plaza North & Crescent St" 375 | 3130,40.75325964,-73.94335788,"21 St & Queens Plaza North" 376 | 3131,40.76712840349542,-73.96224617958069,"E 68 St & 3 Ave" 377 | 3132,40.76350532,-73.97109243,"E 59 St & Madison Ave" 378 | 3134,40.76312584,-73.96526895,"3 Ave & E 62 St" 379 | 3135,40.77112927,-73.95772297,"E 75 St & 3 Ave" 380 | 3136,40.766368,-73.971518,"5 Ave & E 63 St" 381 | 3137,40.77282817,-73.96685276,"5 Ave & E 73 St" 382 | 3139,40.77118287540658,-73.96409422159195,"E 72 St & Park Ave" 383 | 3140,40.77140426,-73.9535166,"1 Ave & E 78 St" 384 | 3141,40.76500525,-73.95818491,"1 Ave & E 68 St" 385 | 3142,40.7612274,-73.96094022,"1 Ave & E 62 St" 386 | 3143,40.77632142182271,-73.96427392959595,"5 Ave & E 78 St" 387 | 3144,40.77677702,-73.9590097,"E 81 St & Park Ave" 388 | 3145,40.77862688,-73.95772073,"E 84 St & Park Ave" 389 | 3146,40.77573034,-73.9567526,"E 81 St & 3 Ave" 390 | 3147,40.77801203,-73.95407149,"E 85 St & 3 Ave" 391 | 3148,40.77565541,-73.95068615,"E 84 St & 1 Ave" 392 | 3150,40.77536905,-73.94803392,"E 85 St & York Ave" 393 | 3151,40.7728384,-73.94989233,"E 81 St & York Ave" 394 | 3152,40.76873687,-73.96119945,"3 Ave & E 71 St" 395 | 3155,40.76440023,-73.96648977,"Lexington Ave & E 63 St" 396 | 3156,40.76663814,-73.95348296,"E 72 St & York Ave" 397 | 3157,40.77518615,-73.94446054,"East End Ave & E 86 St" 398 | 3158,40.77163851,-73.98261428,"W 63 St & Broadway" 399 | 3159,40.77492513,-73.98266566,"W 67 St & Broadway" 400 | 3160,40.77896784,-73.97374737,"Central Park West & W 76 St" 401 | 3161,40.7801839724239,-73.97728532552719,"W 76 St & Columbus Ave" 402 | 3162,40.78339981,-73.98093133,"W 78 St & Broadway" 403 | 3163,40.7734066,-73.97782542,"Central Park West & W 68 St" 404 | 3164,40.7770575,-73.97898475,"Columbus Ave & W 72 St" 405 | 3165,40.77579376683666,-73.9762057363987,"Central Park West & W 72 St" 406 | 3166,40.78057799010334,-73.98562431335449,"Riverside Dr & W 72 St" 407 | 3167,40.77966809007312,-73.98093044757842,"Amsterdam Ave & W 73 St" 408 | 3168,40.78472675,-73.96961715,"Central Park West & W 85 St" 409 | 3169,40.78720869,-73.98128127,"Riverside Dr & W 82 St" 410 | 3170,40.78499979,-73.97283406,"W 84 St & Columbus Ave" 411 | 3171,40.78524672,-73.97667321,"Amsterdam Ave & W 82 St" 412 | 3172,40.7785669,-73.97754961,"W 74 St & Columbus Ave" 413 | 3173,40.777507027547976,-73.98888587951659,"Riverside Blvd & W 67 St" 414 | 3175,40.77748046,-73.98288594,"W 70 St & Amsterdam Ave" 415 | 3176,40.77452835,-73.98753759,"W 64 St & West End Ave" 416 | 3177,40.7867947,-73.977112,"W 84 St & Broadway" 417 | 3178,40.78414472,-73.98362492,"Riverside Dr & W 78 St" 418 | 3179,40.698617,-73.941342,"Park Ave & Marcus Garvey Blvd" 419 | 3180,40.69878,-73.99712,"Brooklyn Bridge Park - Pier 2" 420 | 3182,40.686931,-74.016966,"Yankee Ferry Terminal" 421 | 3221,40.743,-73.93561,"47 Ave & 31 St" 422 | 3223,40.758996559605116,-73.96865397691727,"E 55 St & 3 Ave" 423 | 3224,40.73997354103409,-74.00513872504234,"W 13 St & Hudson St" 424 | 3226,40.78275,-73.97137,"W 82 St & Central Park West" 425 | 3231,40.76780080148132,-73.96592080593109,"E 67 St & Park Ave" 426 | 3232,40.68962188790333,-73.98304268717766,"Bond St & Fulton St" 427 | 3233,40.75724567911726,-73.97805914282799,"E 48 St & 5 Ave" 428 | 3235,40.752165280621966,-73.97992193698882,"E 41 St & Madison Ave" 429 | 3236,40.75898481399634,-73.99379968643188,"W 42 St & Dyer Ave" 430 | 3238,40.77391390238118,-73.9543953537941,"E 80 St & 2 Ave" 431 | 3241,40.686203,-73.944694,"Monroe St & Tompkins Ave" 432 | 3242,40.69102925677968,-73.99183362722397,"Schermerhorn St & Court St" 433 | 3243,40.75892386377695,-73.96226227283478,"E 58 St & 1 Ave" 434 | 3244,40.73143724085228,-73.99490341544151,"University Pl & E 8 St" 435 | 3245,40.65594917783975,-74.00835871696472,"Industry City, Building 1 Basement" 436 | 3249,40.6803560840434,-73.9476791024208,"Verona Pl & Fulton St" 437 | 3250,40.71690978045965,-73.98383796215057,"NYCBS Depot - PIT" 438 | 3254,40.69231660719192,-74.01486575603485,"Soissons Landing" 439 | 3255,40.7505853470215,-73.9946848154068,"8 Ave & W 31 St" 440 | 3256,40.7277140777778,-74.01129573583603,"Pier 40 - Hudson River Park" 441 | 3258,40.75018156325683,-74.00218427181244,"W 27 St & 10 Ave" 442 | 3259,40.74937024193277,-73.99923384189606,"9 Ave & W 28 St" 443 | 3260,40.72706363348306,-73.99662137031554,"Mercer St & Bleecker St" 444 | 3263,40.72923649910006,-73.99086803197861,"Cooper Square & E 7 St" 445 | 3276,40.71458403535893,-74.04281705617905,"Marin Light Rail" 446 | 3282,40.78307,-73.95939,"5 Ave & E 88 St" 447 | 3283,40.7882213,-73.97041561,"W 89 St & Columbus Ave" 448 | 3284,40.781410700190015,-73.95595908164978,"E 88 St & Park Ave" 449 | 3285,40.78839,-73.9747,"W 87 St & Amsterdam Ave" 450 | 3286,40.7806284,-73.9521667,"E 89 St & 3 Ave" 451 | 3288,40.778301,-73.9488134,"E 88 St & 1 Ave" 452 | 3289,40.79017948095081,-73.97288918495178,"W 90 St & Amsterdam Ave" 453 | 3290,40.7779453,-73.946041,"E 89 St & York Ave" 454 | 3292,40.7857851,-73.957481,"5 Ave & E 93 St" 455 | 3293,40.7921,-73.9739,"W 92 St & Broadway" 456 | 3294,40.7835016,-73.955327,"E 91 St & Park Ave" 457 | 3295,40.79127,-73.964839,"Central Park W & W 96 St" 458 | 3297,40.6686627,-73.97988067,"6 St & 7 Ave" 459 | 3298,40.686371,-73.99383324,"Warren St & Court St" 460 | 3300,40.66514681533792,-73.97637605667114,"Prospect Park West & 8 St" 461 | 3301,40.7919557,-73.968087,"Columbus Ave & W 95 St" 462 | 3302,40.7969347,-73.9643412291,"Columbus Ave & W 103 St" 463 | 3303,40.6849894,-73.99440329,"Butler St & Court St" 464 | 3304,40.668127,-73.98377641,"6 Ave & 9 St" 465 | 3305,40.78112229934166,-73.94965589046478,"E 91 St & 2 Ave" 466 | 3306,40.6662078,-73.98199886,"10 St & 7 Ave" 467 | 3307,40.7941654,-73.974124,"West End Ave & W 94 St" 468 | 3308,40.6861758,-73.99645295,"Kane St & Clinton St" 469 | 3309,40.7859201,-73.94860294,"E 97 St & 3 Ave" 470 | 3310,40.663779,-73.98396846,"14 St & 7 Ave" 471 | 3311,40.68763155,-74.0016256,"Columbia St & Kane St" 472 | 3312,40.7817212,-73.94594,"1 Ave & E 94 St" 473 | 3313,40.6663181,-73.9854617,"6 Ave & 12 St" 474 | 3314,40.7937704,-73.971888,"W 95 St & Broadway" 475 | 3315,40.6847514,-73.99917254,"Henry St & Degraw St" 476 | 3316,40.7989937,-73.9662173778,"W 104 St & Amsterdam Ave" 477 | 3317,40.6686273,-73.98700053,"10 St & 5 Ave" 478 | 3318,40.7839636,-73.9471673,"2 Ave & E 96 St" 479 | 3319,40.666287,-73.98895053,"14 St & 5 Ave" 480 | 3320,40.793393,-73.9635556,"Central Park West & W 100 St" 481 | 3321,40.6831164,-73.99785267,"Clinton St & Union St" 482 | 3322,40.668603,-73.9904394,"12 St & 4 Ave" 483 | 3323,40.7981856,-73.9605909006,"W 106 St & Central Park West" 484 | 3324,40.6685455,-73.99333264,"3 Ave & 14 St" 485 | 3325,40.7849032,-73.950503,"E 95 St & 3 Ave" 486 | 3326,40.67434,-74.00194698,"Clinton St & Centre St" 487 | 3327,40.7877214,-73.94728331,"3 Ave & E 100 St" 488 | 3328,40.795,-73.9645,"W 100 St & Manhattan Ave" 489 | 3329,40.6829151,-73.99318208,"Degraw St & Smith St" 490 | 3330,40.6725058,-74.00494695,"Henry St & Bay St" 491 | 3331,40.8013434,-73.9711457439,"Riverside Dr & W 104 St" 492 | 3332,40.681990442707026,-73.99079024791718,"Degraw St & Hoyt St" 493 | 3333,40.6747055,-74.0075572,"Columbia St & Lorraine St" 494 | 3335,40.6772744,-73.98282002,"Union St & 4 Ave" 495 | 3336,40.787801,-73.953559,"E 97 St & Madison Ave" 496 | 3337,40.67363551341504,-74.01195555925369,"Dwight St & Van Dyke St" 497 | 3338,40.7862586,-73.94552579,"2 Ave & E 99 St" 498 | 3339,40.6765304,-73.97846879,"Berkeley Pl & 6 Ave" 499 | 3340,40.6753274,-74.0100698,"Wolcott St & Dwight St" 500 | 3341,40.795346,-73.96186,"Central Park West & W 102 St" 501 | 3342,40.6777748,-74.0094613,"Pioneer St & Richards St" 502 | 3343,40.7997568,-73.9621128676,"W 107 St & Columbus Ave" 503 | 3344,40.679043,-74.011169,"Pioneer St & Van Brunt St" 504 | 3345,40.78948541553215,-73.95242929458618,"Madison Ave & E 99 St" 505 | 3346,40.675146838709786,-73.97523209452629,"Berkeley Pl & 7 Ave" 506 | 3347,40.6773429,-74.01275056,"Van Brunt St & Wolcott St" 507 | 3348,40.677236,-74.015665,"Coffey St & Conover St" 508 | 3349,40.6729679,-73.97087984,"Grand Army Plaza & Plaza St West" 509 | 3350,40.7973721,-73.97041192,"W 100 St & Broadway" 510 | 3351,40.7869946,-73.94164802,"E 102 St & 1 Ave" 511 | 3352,40.67267243410948,-74.0087952464819,"Sigourney St & Columbia St" 512 | 3353,40.6747844,-74.01612847,"Reed St & Van Brunt St" 513 | 3354,40.668132,-73.97363831,"3 St & Prospect Park West" 514 | 3355,40.76800889305947,-73.96845281124115,"E 66 St & Madison Ave" 515 | 3356,40.7746671,-73.98470567,"Amsterdam Ave & W 66 St" 516 | 3357,40.8008363,-73.9664492472,"W 106 St & Amsterdam Ave" 517 | 3358,40.6711978,-73.97484126,"Garfield Pl & 8 Ave" 518 | 3359,40.7691572,-73.96703464,"E 68 St & Madison Ave" 519 | 3360,40.7829391,-73.9786517,"Amsterdam Ave & W 79 St" 520 | 3361,40.6740886,-73.9787282,"Carroll St & 6 Ave" 521 | 3362,40.7781314,-73.96069399,"Madison Ave & E 82 St" 522 | 3363,40.7904828,-73.95033068,"E 102 St & Park Ave" 523 | 3364,40.6751622,-73.9814832,"Carroll St & 5 Ave" 524 | 3365,40.6703837,-73.97839676,"3 St & 7 Ave" 525 | 3366,40.8021174,-73.9681805305,"West End Ave & W 107 St" 526 | 3367,40.7922553,-73.95249933,"5 Ave & E 103 St" 527 | 3368,40.6728155,-73.98352355,"5 Ave & 3 St" 528 | 3369,40.772460677893974,-73.94682079553604,"E 82 St & East End Ave" 529 | 3370,40.7727966,-73.95577801,"E 78 St & 2 Ave" 530 | 3371,40.674613422475815,-73.98501142859459,"4 Ave & 2 St" 531 | 3372,40.7689738,-73.95482273,"E 74 St & 1 Ave" 532 | 3373,40.6750705,-73.98775226,"3 St & 3 Ave" 533 | 3374,40.799484,-73.955613,"Central Park North & Adam Clayton Powell Blvd" 534 | 3375,40.7699426,-73.96060712,"3 Ave & E 72 St" 535 | 3376,40.76471851944339,-73.96222069859505,"E 65 St & 2 Ave" 536 | 3377,40.6786115,-73.99037292,"Carroll St & Bond St" 537 | 3378,40.773763,-73.96222088,"E 76 St & Park Ave" 538 | 3379,40.7903051,-73.94755757,"E 103 St & Lexington Ave" 539 | 3381,40.6777287,-73.99364123,"3 St & Hoyt St" 540 | 3382,40.680611,-73.99475825,"Carroll St & Smith St" 541 | 3383,40.804213,-73.96699104,"Cathedral Pkwy & Broadway" 542 | 3384,40.6787242,-73.99599099,"Smith St & 3 St" 543 | 3386,40.6809591,-73.99905709,"1 Pl & Clinton St" 544 | 3387,40.7934337,-73.94945003,"E 106 St & Madison Ave" 545 | 3388,40.6828003,-73.99990419,"President St & Henry St" 546 | 3389,40.6830456,-74.00348559,"Carroll St & Columbia St" 547 | 3390,40.79329668,-73.9432083,"E 109 St & 3 Ave" 548 | 3391,40.7892529,-73.93956237,"E 106 St & 1 Ave" 549 | 3392,40.6812117,-74.00860912,"Commerce St & Van Brunt St" 550 | 3393,40.6794327,-74.00785041,"Richards St & Delavan St" 551 | 3394,40.6769993,-74.00647134,"Columbia St & W 9 St" 552 | 3395,40.6763744,-74.00324957,"Henry St & W 9 St" 553 | 3396,40.6783563,-74.00014502,"Clinton St & 4 Place" 554 | 3397,40.6763947,-73.99869893,"Court St & Nelson St" 555 | 3398,40.6746957,-73.99785768,"Smith St & 9 St" 556 | 3399,40.67260298150126,-73.98983001708984,"7 St & 3 Ave" 557 | 3400,40.7961535,-73.94782145,"E 110 St & Madison Ave" 558 | 3401,40.6724811,-73.99331394,"2 Ave & 9 St" 559 | 3402,40.6902375,-73.99203074,"Court St & State St" 560 | 3403,40.6705135,-73.98876585,"4 Ave & 9 St" 561 | 3404,40.6704922,-73.98541675,"7 St & 5 Ave" 562 | 3405,40.6704836,-73.98208968,"5 St & 6 Ave" 563 | 3407,40.67909799721684,-73.98765474557877,"Union St & Nevins St" 564 | 3408,40.6881529,-73.99520919,"Congress St & Clinton St" 565 | 3409,40.6867443,-73.99063168,"Bergen St & Smith St" 566 | 3410,40.6864442,-73.98759104,"Dean St & Hoyt St" 567 | 3411,40.6849668,-73.98620772,"Bond St & Bergen St" 568 | 3412,40.6853761,-73.98302136,"Pacific St & Nevins St" 569 | 3413,40.6827549,-73.98258555,"Wyckoff St & 3 Ave" 570 | 3414,40.680944723477296,-73.97567331790923,"Bergen St & Flatbush Ave" 571 | 3415,40.6793307,-73.97519523,"Prospect Pl & 6 Ave" 572 | 3416,40.6776147,-73.97324283,"7 Ave & Park Pl" 573 | 3417,40.6795766,-73.97854971,"Baltic St & 5 Ave" 574 | 3418,40.6750207,-73.97111473,"Plaza St West & Flatbush Ave" 575 | 3419,40.6792788,-73.98154004,"Douglass St & 4 Ave" 576 | 3420,40.6802133,-73.98432695,"Douglass St & 3 Ave" 577 | 3421,40.6843549,-73.98901629,"Hoyt St & Warren St" 578 | 3422,40.6859296,-74.00242364,"Columbia St & Degraw St" 579 | 3423,40.6610633719006,-73.97945255041122,"West Drive & Prospect Park West" 580 | 3424,40.791976,-73.945993,"E 106 St & Lexington Ave" 581 | 3425,40.7892105,-73.94370784,"2 Ave & E 104 St" 582 | 3427,40.72430527250332,-73.99600982666016,"Lafayette St & Jersey St" 583 | 3428,40.740983,-74.001702,"8 Ave & W 16 St" 584 | 3429,40.68506807308177,-73.97790759801863,"Hanson Pl & Ashland Pl" 585 | 3430,40.71907891179564,-73.94223690032959,"Richardson St & N Henry St" 586 | 3431,40.746524,-73.977885,"E 35 St & 3 Ave" 587 | 3432,40.66906013501107,-73.99463653564453,"NYCBS Depot - GOW" 588 | 3434,40.79025417330419,-73.97718340158461,"W 88 St & West End Ave" 589 | 3435,40.718822,-73.99596,"Grand St & Elizabeth St" 590 | 3436,40.721319,-74.010065,"Greenwich St & Hubert St" 591 | 3438,40.772248537721744,-73.95842134952545,"E 76 St & 3 Ave" 592 | 3440,40.692418292578466,-73.98949474096298,"Fulton St & Adams St" 593 | 3441,40.752957,-74.00264,"10 Hudson Yards" 594 | 3443,40.76132983124814,-73.97982001304626,"W 52 St & 6 Ave" 595 | 3445,40.79181171501097,-73.9786022901535,"Riverside Dr & W 89 St" 596 | 3447,40.76703432309872,-73.95622730255127,"E 71 St & 1 Ave" 597 | 3449,40.721462562298164,-73.94800901412964,"Eckford St & Engert Ave" 598 | 3452,40.71915571696044,-73.94885390996933,"Bayard St & Leonard St" 599 | 3453,40.71335226222875,-73.9491033554077,"Devoe St & Lorimer St" 600 | 3454,40.7103685423523,-73.94705951213837,"Leonard St & Maujer St" 601 | 3455,40.68680820503432,-73.98036181926727,"Schermerhorn St & 3 Ave" 602 | 3456,40.71638031973561,-73.94821286201477,"Jackson St & Leonard St" 603 | 3457,40.76302594280519,-73.97209525108337,"E 58 St & Madison Ave" 604 | 3458,40.76309387270797,-73.9783501625061,"W 55 St & 6 Ave" 605 | 3459,40.75763227739443,-73.96930575370789,"E 53 St & 3 Ave" 606 | 3461,40.714851505262516,-74.01122331619263,"Murray St & Greenwich St" 607 | 3462,40.75118387463277,-73.97138714790344,"E 44 St & 2 Ave" 608 | 3463,40.735367055605394,-73.98797392845154,"E 16 St & Irving Pl" 609 | 3464,40.75227093837409,-73.98770570755005,"W 37 St & Broadway" 610 | 3466,40.75668720603179,-73.98257732391357,"W 45 St & 6 Ave" 611 | 3468,40.7303801,-73.9747502,"NYCBS Depot - STY - Garage 4" 612 | 3469,40.73181401720966,-73.95995020866394,"India St & West St" 613 | 3472,40.742753828659026,-74.00747358798981,"W 15 St & 10 Ave" 614 | 3474,40.7252556952547,-74.00412082672119,"6 Ave & Spring St" 615 | 3476,40.72576996727214,-73.9507395029068,"Norman Ave & Leonard St" 616 | -------------------------------------------------------------------------------- /images/citibike-trips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/citibike-trips.png -------------------------------------------------------------------------------- /images/citibike-trips.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/citibike-trips.sketch -------------------------------------------------------------------------------- /images/citibike.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/citibike.mov -------------------------------------------------------------------------------- /images/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/design.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | citi bike commute 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 150 | 151 | 152 |
Loading a couple megabytes of data. This should only take a few seconds. If you're on a slow connection, this might take a little longer.
153 | 169 | 170 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /old-code/main.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | import csv 3 | 4 | import numpy 5 | from Pycluster import * 6 | 7 | data_dir = 'data/' 8 | paths = ['2013-07.csv', '2013-08.csv', '2013-09.csv', '2013-10.csv', 9 | '2013-11.csv', '2013-12.csv', '2014-01.csv', '2014-02.csv'] 10 | 11 | counts = [] 12 | headers = [] 13 | genders = {} 14 | usertypes = {} 15 | birthyears = {} 16 | rides = {} 17 | routes = {} 18 | stations = {} 19 | 20 | def crunch_numbers(): 21 | most_traveled = 0 22 | most_traveled_alert = 'I got nothin' 23 | 24 | for i in range(len(paths)): 25 | ifile = open(data_dir + paths[i], 'rb') 26 | reader = csv.reader(ifile) 27 | counts.append(0) 28 | for row in reader: 29 | if counts[i] == 0: 30 | headers.append(row) 31 | else: 32 | gender = row[14] 33 | usertype = row[12] 34 | birthyear = row[13] 35 | bikeid = row[11] 36 | start_station = row[3] 37 | start_station_name = row[4] 38 | start_station_lat = row[5] 39 | start_station_long = row[6] 40 | end_station = row[7] 41 | end_station_name = row[8] 42 | end_station_lat = row[9] 43 | end_station_long = row[10] 44 | 45 | genders.setdefault(gender, 0) 46 | usertypes.setdefault(usertype, 0) 47 | birthyears.setdefault(birthyear, 0) 48 | rides.setdefault(bikeid, 0) 49 | routes.setdefault(start_station, {}) 50 | routes[start_station].setdefault(end_station, 0) 51 | 52 | station_info = { 53 | 'id': start_station, 54 | 'name': start_station_name, 55 | 'lat': start_station_lat, 56 | 'long': start_station_long 57 | } 58 | 59 | stations.setdefault(start_station, station_info) 60 | 61 | if start_station != end_station: 62 | station_info = { 63 | 'id': end_station, 64 | 'name': end_station_name, 65 | 'lat': end_station_lat, 66 | 'long': end_station_long 67 | } 68 | stations.setdefault(end_station, station_info) 69 | 70 | genders[gender] += 1 71 | usertypes[usertype] += 1 72 | birthyears[birthyear] += 1 73 | rides[bikeid] += 1 74 | routes[start_station][end_station] += 1 75 | 76 | trips = routes[start_station][end_station] 77 | if trips > most_traveled: 78 | most_traveled = trips 79 | most_traveled_alert = '%s -> %s : %d trips' % (start_station_name, end_station_name, trips) 80 | 81 | counts[i] += 1 82 | 83 | print('%d percent done' % (float(i + 1) / len(paths) * 100)) 84 | 85 | # just check and make sure all the headers are the same 86 | for j in range(len(headers)): 87 | if j == 0: continue 88 | if headers[j] != headers[j - 1]: 89 | print("%s does not equal %s" % (j, j - 1)) 90 | 91 | years = birthyears.keys() 92 | years.sort() 93 | 94 | print('Creating station file data') 95 | f = open(data_dir + 'station_data.csv', 'wb') 96 | writer = csv.writer(f, delimiter='\t') 97 | [writer.writerow(s.values()) for s in stations.values()] 98 | 99 | print('\n') 100 | print('Header: %r' % headers[0]) 101 | print('\n') 102 | print('Count: %r' % sum(counts)) 103 | print('Genders: %r' % genders) 104 | print('User Types: %r' % usertypes) 105 | print('Average number of rides per bike: %d' % (sum(rides.values()) / len(rides))) 106 | print('Station Count: %d' % len(stations)) 107 | print(most_traveled_alert) 108 | print('\n') 109 | 110 | crunch_numbers() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "citibike-trips", 3 | "version": "0.0.2", 4 | "scripts": { 5 | "start": "budo src/index.js:bundle.js --live --open", 6 | "build": "browserify src/index.js > bundle.js", 7 | "serve": "open http://localhost:8080 && python -m SimpleHTTPServer 8080", 8 | "lint": "standard" 9 | }, 10 | "dependencies": { 11 | "3d-view-controls": "^2.2.0", 12 | "argv": "^0.0.2", 13 | "browserify": "^14.4.0", 14 | "budo": "^10.0.3", 15 | "camera-picking-ray": "^1.0.1", 16 | "canvas-fit": "^1.5.0", 17 | "cat-rom-spline": "^1.0.0", 18 | "color": "^2.0.0", 19 | "d3-geo": "^1.6.4", 20 | "dat-gui": "^0.5.0", 21 | "dom-css": "^2.1.0", 22 | "eslint": "^4.2.0", 23 | "fast-csv": "^2.4.0", 24 | "geojson-bbox": "^0.0.0", 25 | "gl-mat4": "^1.1.4", 26 | "gl-vec2": "^1.0.0", 27 | "glslify": "^6.1.0", 28 | "lerp": "^1.0.3", 29 | "new-array": "^1.0.0", 30 | "projections": "^1.0.0", 31 | "ray-plane-intersection": "^1.0.0", 32 | "regl": "^1.3.0", 33 | "smoothstep": "^1.0.1", 34 | "spring-animator": "^1.0.3", 35 | "standard": "^10.0.2", 36 | "topojson": "^3.0.0" 37 | }, 38 | "browserify": { 39 | "transform": [ 40 | "glslify" 41 | ] 42 | }, 43 | "eslintConfig": { 44 | "extends": [ 45 | "standard", 46 | "standard-jsx" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scripts/clean-citibike-trips.js: -------------------------------------------------------------------------------- 1 | // go through all citibike trip csv data and output two files: 2 | // 1. stations.csv - station_id,lat,lon,name 3 | // 2. trips.csv - start_ts,duration,start_station,end_station,subscriber,birth_year 4 | 5 | const fs = require('fs') 6 | const csv = require('fast-csv') 7 | 8 | const STATIONS_OUT = './stations.csv' 9 | const stationsHeader = 'station_id,latitude,longitude,name' 10 | const stations = {} 11 | 12 | process.stdout.write('start_ts,duration,start_station,end_station,subscriber,birth_year\n') 13 | 14 | csv 15 | .fromStream(process.stdin, { headers: true }) 16 | .on('data', processLine) 17 | .on('end', onEnd) 18 | 19 | function processLine (data) { 20 | const startTs = data['starttime'] 21 | const duration = int(data['tripduration']) 22 | const startStation = int(data['start station id']) 23 | const endStation = int(data['end station id']) 24 | const subscriber = data['usertype'] === 'Subscriber' ? 1 : 0 25 | const birthYear = int(data['birth year']) || 0 26 | 27 | if (!startStation || !endStation || !duration || !startTs) return 28 | 29 | process.stdout.write(`${startTs},${duration},${startStation},${endStation},${subscriber},${birthYear}\n`) 30 | 31 | if (!stations[startStation]) { 32 | stations[startStation] = [ 33 | startStation, 34 | data['start station latitude'], 35 | data['start station longitude'], 36 | `"${data['start station name']}"` 37 | ].join(',') 38 | } 39 | if (!stations[endStation]) { 40 | stations[endStation] = [ 41 | endStation, 42 | data['end station latitude'], 43 | data['end station longitude'], 44 | `"${data['end station name']}"` 45 | ].join(',') 46 | } 47 | } 48 | 49 | function onEnd () { 50 | const csvText = [ 51 | stationsHeader, 52 | ...Object.keys(stations).map(id => stations[id]) 53 | ].join('\n') 54 | 55 | fs.writeFileSync(STATIONS_OUT, csvText + '\n') 56 | } 57 | 58 | function int (val) { 59 | return parseInt(val, 10) 60 | } 61 | -------------------------------------------------------------------------------- /scripts/download-data.sh: -------------------------------------------------------------------------------- 1 | # curl "https://s3.amazonaws.com/tripdata/201707-citibike-tripdata.csv.zip" > 201707-citibike-tripdata.csv.zip 2 | # curl "https://s3.amazonaws.com/tripdata/201706-citibike-tripdata.csv.zip" > 201706-citibike-tripdata.csv.zip 3 | # curl "https://s3.amazonaws.com/tripdata/201705-citibike-tripdata.csv.zip" > 201705-citibike-tripdata.csv.zip 4 | # curl "https://s3.amazonaws.com/tripdata/201704-citibike-tripdata.csv.zip" > 201704-citibike-tripdata.csv.zip 5 | 6 | # NOTE: columns for data before april 2017 may be named differently than scripts are expecting 7 | 8 | # curl "https://s3.amazonaws.com/tripdata/201703-citibike-tripdata.csv.zip" > 201703-citibike-tripdata.csv.zip 9 | # curl "https://s3.amazonaws.com/tripdata/201702-citibike-tripdata.csv.zip" > 201702-citibike-tripdata.csv.zip 10 | # curl "https://s3.amazonaws.com/tripdata/201701-citibike-tripdata.csv.zip" > 201701-citibike-tripdata.csv.zip 11 | 12 | # NOTE: pre-2017 data has dates represented a little differently and may not work seamlessly with this codebase 13 | 14 | # curl "https://s3.amazonaws.com/tripdata/201612-citibike-tripdata.zip" > 201612-citibike-tripdata.csv.zip 15 | # curl "https://s3.amazonaws.com/tripdata/201611-citibike-tripdata.zip" > 201611-citibike-tripdata.csv.zip 16 | # ... and so on 17 | -------------------------------------------------------------------------------- /scripts/extract-coordinates-from-geojson.js: -------------------------------------------------------------------------------- 1 | // clean nyc-streets.geo.json 2 | const fs = require('fs') 3 | const path = require('path') 4 | const topojson = require('topojson') 5 | const argv = require('argv') 6 | const args = argv.option({ 7 | name: 'help', 8 | short: 'h', 9 | type: 'boolean' 10 | }).run() 11 | 12 | const pathToGeoJSON = args.targets[0] 13 | 14 | if (args.options.help || !pathToGeoJSON) help() 15 | 16 | function help () { 17 | console.log(`usage: node ${path.basename(__filename)} PATH/TO/GEOJSON.json`) 18 | process.exit() 19 | } 20 | 21 | const json = fs.readFileSync(pathToGeoJSON, 'utf-8') 22 | const geoJSON = JSON.parse(json) 23 | 24 | if (geoJSON.type === 'Topology') { 25 | const result = topojson.mesh(geoJSON) 26 | console.log('TOPOLOGY! Check the code and figure out how to do this') 27 | console.log(result.coordinates.length) 28 | process.exit() 29 | } 30 | 31 | geoJSON.features.forEach(feat => { 32 | // filter to just BK and Manhattan streets 33 | // if (!['047', '061'].includes(feat.properties['COUNTYFP'])) return 34 | // if (feat.properties['COUNTYFP'] !== '061') return 35 | // if (feat.properties['MTFCC'] !== 'S1200' && feat.properties['MTFCC'] !== 'S1100') return 36 | if (feat.geometry.type === 'MultiLineString') feat.geometry.coordinates.forEach(processLine) 37 | if (feat.geometry.type === 'LineString') processLine(feat.geometry.coordinates) 38 | if (feat.geometry.type === 'Polygon') feat.geometry.coordinates.forEach(processLine) 39 | if (feat.geometry.type === 'MultiPolygon') feat.geometry.coordinates.forEach(c => c.forEach(processLine)) 40 | }) 41 | 42 | function processLine (line) { 43 | process.stdout.write(line.map(pt => pt.join(',')).join(';')) 44 | process.stdout.write('\n') 45 | } 46 | -------------------------------------------------------------------------------- /scripts/process-trips.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # NOTE: this should be called from /data 4 | 5 | IN="201704-citibike-tripdata.csv" 6 | DATE="2017-04-18" 7 | OUT="trips-$DATE.csv" 8 | CLEANED_DIR="../cleaned" 9 | 10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 11 | 12 | head -1 $IN > $OUT 13 | cat $IN | grep $DATE >> $OUT 14 | cat $OUT | node $DIR/clean-citibike-trips.js > $OUT.tmp 15 | head -1 $OUT.tmp > $CLEANED_DIR/$OUT 16 | cat $OUT.tmp | grep $DATE >> $CLEANED_DIR/$OUT 17 | rm $OUT.tmp $OUT 18 | -------------------------------------------------------------------------------- /src/check-support.js: -------------------------------------------------------------------------------- 1 | module.exports = function checkSupport (regl) { 2 | const data = new Float32Array(4) 3 | data[0] = data[1] = data[2] = data[3] = 0.1234567 4 | const initialTexture = regl.texture({ 5 | data: data, 6 | shape: [1, 1, 4], 7 | type: 'float' 8 | }) 9 | try { 10 | regl.framebuffer({ 11 | color: initialTexture, 12 | depth: false, 13 | stencil: false 14 | }) 15 | } catch (err) { 16 | notSupported() 17 | throw new Error('visualization requires OES_texture_float webgl extension', err) 18 | } 19 | } 20 | 21 | function notSupported () { 22 | document.body.innerHTML = '' 23 | const warningDiv = document.body.appendChild(document.createElement('div')) 24 | warningDiv.classList.add('not-supported') 25 | warningDiv.innerHTML = 'This visualization requires a WebGL ' + 26 | 'extension that may not be available on mobile browsers. But you can take a look at ' + 27 | 'this little video I made of it!' 28 | } 29 | -------------------------------------------------------------------------------- /src/create-buttons.js: -------------------------------------------------------------------------------- 1 | module.exports = function createButtons (container, settings) { 2 | const createBtnEl = () => document.createElement('span') 3 | const buttons = [ 4 | { name: 'showPoints', label: 'Points', group: 'view-options', el: createBtnEl() }, 5 | { name: 'showPaths', label: 'Paths', group: 'view-options', el: createBtnEl() }, 6 | { name: 'curvedPaths', label: 'Arcs', group: 'view-options', el: createBtnEl() }, 7 | 8 | { name: 'subscriber', label: 'Member', group: 'filter', el: createBtnEl() }, 9 | { name: 'nonSubscriber', label: 'Day Pass', group: 'filter', el: createBtnEl() } 10 | ] 11 | 12 | const btnGroups = {} 13 | buttons.forEach(({ name, label, el, group }) => { 14 | btnGroups[group] = btnGroups[group] || createAndAppendButtonGroup(container, group) 15 | el.innerText = label 16 | btnGroups[group].appendChild(el) 17 | el.addEventListener('click', () => toggleFilter(name)) 18 | }) 19 | 20 | return function renderButtons (settings) { 21 | buttons.forEach(({ name, el }) => { 22 | if (settings[name]) { 23 | el.classList.add('highlight') 24 | } else { 25 | el.classList.remove('highlight') 26 | } 27 | }) 28 | } 29 | 30 | function toggleFilter (name) { 31 | settings[name] = !settings[name] 32 | } 33 | } 34 | 35 | function createAndAppendButtonGroup (el, name) { 36 | const btnGroupEl = el.appendChild(document.createElement('div')) 37 | btnGroupEl.classList.add('button-group') 38 | btnGroupEl.classList.add(name) 39 | return btnGroupEl 40 | } 41 | -------------------------------------------------------------------------------- /src/create-elapsed-time-view.js: -------------------------------------------------------------------------------- 1 | const { secondsToTime, getMeridian, convertHourFrom24H } = require('./helpers') 2 | 3 | module.exports = function createElapsedTimeView (el, trips) { 4 | // assuming trips are in order, we'll say the current time of the viz 5 | // is simply midnight of the day of the first trip in the list plus 6 | // elapsed time 7 | const firstTripStartDate = trips[0]['start_ts'].split(' ')[0] 8 | const vizStartTime = (new Date(`${firstTripStartDate}T00:00:00`)).getTime() 9 | 10 | const timeEl = el.querySelector('span.time') 11 | const meridanEl = el.querySelector('span.meridian') 12 | const dayEl = el.querySelector('.day') 13 | const dateEl = el.querySelector('.date') 14 | 15 | return function renderElapsedTime (elapsed) { 16 | const curDate = new Date(vizStartTime + elapsed * 1000) 17 | const curTime = secondsToTime(elapsed) 18 | dayEl.innerText = days[curDate.getDay()] 19 | dateEl.innerText = `${curDate.getDate()} ${months[curDate.getMonth()]} ${curDate.getFullYear()}` 20 | timeEl.innerText = convertHourFrom24H(curTime) 21 | meridanEl.innerText = getMeridian(curTime) 22 | } 23 | } 24 | 25 | const days = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday'.split(' ') 26 | const months = 'January February March April May June July August September October November December'.split(' ') 27 | -------------------------------------------------------------------------------- /src/create-map-renderer.js: -------------------------------------------------------------------------------- 1 | const glslify = require('glslify') 2 | 3 | module.exports = function createMapRenderer (regl, lines) { 4 | const lineSegments = [] 5 | for (let line of lines) { 6 | for (let j = 0; j < line.length - 1; j++) { 7 | lineSegments.push(line[j]) 8 | lineSegments.push(line[j + 1]) 9 | } 10 | } 11 | 12 | const renderMap = regl({ 13 | vert: glslify.file('./map.vert'), 14 | frag: glslify.file('./simple.frag'), 15 | attributes: { 16 | position: lineSegments 17 | }, 18 | count: lineSegments.length, 19 | uniforms: { 20 | color: [0.75, 0.75, 0.75] 21 | }, 22 | primitive: 'lines' 23 | }) 24 | 25 | const gridLines = [] 26 | const separation = 0.05 27 | const limit = 2 28 | for (let j = -limit; j <= limit; j += separation) { 29 | gridLines.push([j, -limit, 0], [j, limit, 0], [-limit, j, 0], [limit, j, 0]) 30 | } 31 | 32 | const renderGrid = regl({ 33 | vert: glslify.file('./simple.vert'), 34 | frag: glslify.file('./simple.frag'), 35 | attributes: { 36 | position: gridLines 37 | }, 38 | count: gridLines.length, 39 | uniforms: { 40 | color: [0.15, 0.15, 0.15, 0.25] 41 | }, 42 | primitive: 'lines' 43 | }) 44 | 45 | return () => { 46 | renderGrid() 47 | renderMap() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/create-projection.js: -------------------------------------------------------------------------------- 1 | const mercator = require('projections/mercator') 2 | 3 | module.exports = function createProjection ({ bbox, zoom }) { 4 | const projectionOpts = { 5 | meridian: 180 + bbox[0], 6 | latLimit: bbox[3] 7 | } 8 | return function project ([lon, lat]) { 9 | const { x, y } = mercator({ lon, lat }, projectionOpts) 10 | return [x * zoom, y * zoom] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/create-roaming-camera.js: -------------------------------------------------------------------------------- 1 | const { createSpring } = require('spring-animator') 2 | const createCamera = require('3d-view-controls') 3 | const { getIntersection } = require('./helpers') 4 | 5 | module.exports = function createRoamingCamera (canvas, focus, center, eye, getProjection) { 6 | let isRoaming = true 7 | let timeout 8 | 9 | canvas.addEventListener('mousedown', stopRoaming) 10 | canvas.addEventListener('dblclick', onDblClick) 11 | 12 | const camera = createCamera(canvas, { 13 | zoomSpeed: 4, 14 | distanceLimits: [0.05, 1.03] 15 | }) 16 | const [fX, fY] = focus 17 | const cameraX = createSpring(0.005, 1.5, center[0]) 18 | const cameraY = createSpring(0.005, 1.5, center[1]) 19 | const cameraZ = createSpring(0.005, 1.5, center[2]) 20 | 21 | const focusX = createSpring(0.05, 1.5, fX) 22 | const focusY = createSpring(0.05, 1.5, fY) 23 | 24 | camera.lookAt( 25 | center, 26 | eye, 27 | [0.52, -0.11, -99] 28 | ) 29 | 30 | function onDblClick (e) { 31 | const [fX, fY] = getIntersection( 32 | [e.clientX, e.clientY], 33 | // prob not the best idea since elsewhere we are using `viewportWidth` 34 | // and `viewportHeight` passed by regl 35 | [0, 0, window.innerWidth, window.innerHeight], 36 | getProjection(), 37 | camera.matrix 38 | ) 39 | setSpringsToCurrentCameraValues() 40 | focusX.updateValue(fX) 41 | focusY.updateValue(fY) 42 | 43 | // clear this text selection nonsense on screen after double click 44 | if (document.selection && document.selection.empty) { 45 | document.selection.empty() 46 | } else if (window.getSelection) { 47 | const sel = window.getSelection() 48 | sel.removeAllRanges() 49 | } 50 | } 51 | 52 | function setRandomCameraPosition () { 53 | // dont move focus too much because it has a much snappier spring 54 | const newFocusX = fX + (Math.random() - 0.5) * 0.01 55 | const newFocusY = fY + (Math.random() - 0.5) * 0.01 56 | focusX.updateValue(newFocusX) 57 | focusY.updateValue(newFocusY) 58 | 59 | cameraX.updateValue(newFocusX + Math.random() - 0.5) 60 | cameraY.updateValue(newFocusY + Math.random() - 0.5) 61 | cameraZ.updateValue(Math.random() * -0.5) 62 | } 63 | 64 | cameraRoamLoop() 65 | function cameraRoamLoop () { 66 | clearTimeout(timeout) 67 | timeout = setTimeout(cameraRoamLoop, 10000) 68 | setRandomCameraPosition() 69 | } 70 | 71 | function tick () { 72 | camera.tick() 73 | camera.up = [camera.up[0], camera.up[1], -999] 74 | camera.eye = [focusX.tick(), focusY.tick(), 0] 75 | if (isRoaming) { 76 | camera.center = [cameraX.tick(), cameraY.tick(), cameraZ.tick()] 77 | } 78 | } 79 | function getMatrix () { 80 | return camera.matrix 81 | } 82 | function getCenter () { 83 | return camera.center 84 | } 85 | function stopRoaming () { 86 | clearTimeout(timeout) 87 | timeout = null 88 | isRoaming = false 89 | } 90 | function startRoaming () { 91 | setSpringsToCurrentCameraValues() 92 | cameraRoamLoop() 93 | isRoaming = true 94 | } 95 | 96 | function setSpringsToCurrentCameraValues () { 97 | focusX.updateValue(camera.center[0], false) 98 | focusY.updateValue(camera.center[1], false) 99 | 100 | cameraX.updateValue(camera.eye[0], false) 101 | cameraY.updateValue(camera.eye[1], false) 102 | cameraZ.updateValue(camera.eye[2], false) 103 | } 104 | 105 | window.camera = camera 106 | return { 107 | tick, 108 | getMatrix, 109 | getCenter, 110 | startRoaming 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/create-state-transitioner.js: -------------------------------------------------------------------------------- 1 | const glslify = require('glslify') 2 | const newArray = require('new-array') 3 | const { getIntersection } = require('./helpers') 4 | 5 | module.exports = function createStateTransitioner (regl, trips, settings) { 6 | let mousePosition = [0, 0] 7 | regl._gl.canvas.addEventListener('mousemove', (e) => { 8 | mousePosition = [e.clientX, e.clientY] 9 | }) 10 | let isShiftPressed = false 11 | document.addEventListener('keydown', (e) => { 12 | if (e.which === 16) isShiftPressed = true 13 | }) 14 | document.addEventListener('keyup', (e) => { 15 | if (e.which === 16) isShiftPressed = false 16 | }) 17 | 18 | const tripStateTextureSize = Math.ceil(Math.sqrt(trips.length)) 19 | const tripStateTextureLength = tripStateTextureSize * tripStateTextureSize 20 | const initialTripState = new Float32Array(tripStateTextureLength * 4) 21 | for (let i = 0; i < tripStateTextureLength; ++i) { 22 | initialTripState[i * 4] = 0 // arcHeight 23 | initialTripState[i * 4 + 1] = 0 // pathAlpha 24 | initialTripState[i * 4 + 2] = 0 // pointSize 25 | } 26 | 27 | let prevTripStateTexture = createStateBuffer(initialTripState, tripStateTextureSize) 28 | let curTripStateTexture = createStateBuffer(initialTripState, tripStateTextureSize) 29 | let nextTripStateTexture = createStateBuffer(initialTripState, tripStateTextureSize) 30 | 31 | const stateIndexes = [] 32 | const tripMetaDataState = new Float32Array(tripStateTextureLength * 4) 33 | for (let j = 0; j < trips.length; j++) { 34 | const tripStateIndexX = j % tripStateTextureSize 35 | const tripStateIndexY = j / tripStateTextureSize | 0 36 | stateIndexes.push([tripStateIndexX / tripStateTextureSize, tripStateIndexY / tripStateTextureSize]) 37 | tripMetaDataState[j * 4] = trips[j].subscriber ? 1.0 : 0.0 // subscriber 38 | tripMetaDataState[j * 4 + 1] = trips[j].startPosition[0] 39 | tripMetaDataState[j * 4 + 2] = trips[j].startPosition[1] 40 | } 41 | const tripMetaDataTexture = createStateBuffer(tripMetaDataState, tripStateTextureSize) 42 | 43 | const dampening = 1.0 44 | const stiffness = 0.1 45 | const MAX_ARC_HEIGHT = 2.5 46 | const MAX_PT_SIZE = 3 47 | 48 | const updateState = regl({ 49 | framebuffer: () => nextTripStateTexture, 50 | 51 | vert: glslify.file('./trip-state.vert'), 52 | frag: glslify.file('./trip-state.frag'), 53 | 54 | attributes: { 55 | position: [ 56 | -1, -1, 57 | 1, -1, 58 | -1, 1, 59 | 1, 1 60 | ] 61 | }, 62 | 63 | uniforms: { 64 | curTripStateTexture: () => curTripStateTexture, 65 | prevTripStateTexture: () => prevTripStateTexture, 66 | tripMetaDataTexture: tripMetaDataTexture, 67 | rayPicker: regl.prop('rayPicker'), 68 | rayPickerThreshold: regl.prop('rayPickerThreshold'), 69 | dampening: dampening, 70 | stiffness: stiffness, 71 | maxArcHeight: MAX_ARC_HEIGHT, 72 | maxPointSize: MAX_PT_SIZE, 73 | showSubscriber: regl.prop('showSubscriber'), 74 | showNonSubscriber: regl.prop('showNonSubscriber'), 75 | curvedPaths: regl.prop('curvedPaths'), 76 | showPaths: regl.prop('showPaths'), 77 | showPoints: regl.prop('showPoints') 78 | }, 79 | 80 | count: 4, 81 | primitive: 'triangle strip' 82 | }) 83 | 84 | const circleGranularity = 30 85 | const drawCircle = regl({ 86 | vert: glslify.file('./pointer-circle.vert'), 87 | frag: glslify.file('./simple.frag'), 88 | attributes: { 89 | rads: newArray(circleGranularity + 1).map((_, i) => Math.PI * 2 * i / circleGranularity) 90 | }, 91 | uniforms: { 92 | center: regl.prop('center'), 93 | size: regl.prop('size'), 94 | projection: regl.prop('projection'), 95 | view: regl.prop('view'), 96 | color: [0.8, 0.7, 1.0] 97 | }, 98 | count: circleGranularity, 99 | primitive: 'line loop' 100 | }) 101 | 102 | function getStateIndexes () { 103 | return stateIndexes 104 | } 105 | 106 | function tick (context) { 107 | const rayPickerThreshold = isShiftPressed ? context.rayPickerThreshold : 10 108 | const rayPicker = isShiftPressed ? getIntersection( 109 | mousePosition, 110 | context.viewport, 111 | context.projection, 112 | context.view 113 | ) || [0, 0, 0] : [0, 0, 0] 114 | cycleStates() 115 | updateState({ 116 | showSubscriber: context.subscriber, 117 | showNonSubscriber: context.nonSubscriber, 118 | curvedPaths: context.curvedPaths, 119 | showPaths: context.showPaths, 120 | showPoints: context.showPoints, 121 | rayPickerThreshold: rayPickerThreshold, 122 | rayPicker: rayPicker 123 | }) 124 | if (isShiftPressed) { 125 | drawCircle({ 126 | center: rayPicker, 127 | size: rayPickerThreshold, 128 | projection: context.projection, 129 | view: context.view 130 | }) 131 | } 132 | } 133 | 134 | function getStateTexture () { 135 | return curTripStateTexture 136 | } 137 | 138 | return { 139 | tick, 140 | getStateTexture, 141 | getStateIndexes 142 | } 143 | 144 | function createStateBuffer (initialState, textureSize) { 145 | const initialTexture = regl.texture({ 146 | data: initialState, 147 | shape: [textureSize, textureSize, 4], 148 | type: 'float' 149 | }) 150 | return regl.framebuffer({ 151 | color: initialTexture, 152 | depth: false, 153 | stencil: false 154 | }) 155 | } 156 | 157 | function cycleStates () { 158 | const tmp = prevTripStateTexture 159 | prevTripStateTexture = curTripStateTexture 160 | curTripStateTexture = nextTripStateTexture 161 | nextTripStateTexture = tmp 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/create-timeline.js: -------------------------------------------------------------------------------- 1 | const catRomSpline = require('cat-rom-spline') 2 | const newArray = require('new-array') 3 | const { createSpring } = require('spring-animator') 4 | const css = require('dom-css') 5 | const { getSeconds, secondsToTime, getMeridian, convertHourFrom24H } = require('./helpers') 6 | 7 | const verticalPadding = 20 8 | 9 | module.exports = function createTimeline (el, trips, vizDuration, setElapsed, settings) { 10 | const bucketGranularity = 60 * 30 11 | // get histogram buckets for both subscribers and non-subscribers 12 | const getBucket = getHistogramBucketsByFilter(trips, bucketGranularity, vizDuration) 13 | const svgEl = createSVG(el) 14 | const { width } = el.getBoundingClientRect() 15 | 16 | const updateHistogram = drawHistogram(svgEl, getBucket(settings)) 17 | drawTimeline(svgEl, vizDuration) 18 | const updateScrubberPosition = createScrubber(svgEl, vizDuration) 19 | 20 | el.addEventListener('mousedown', mouseDown) 21 | el.addEventListener('mousemove', mouseMove) 22 | window.addEventListener('mouseup', mouseUp) 23 | 24 | let isMouseDown = false 25 | let curXPosition = 0 26 | function mouseDown (e) { 27 | isMouseDown = true 28 | curXPosition = Math.max(e.offsetX, 0) 29 | setElapsed(getTimeFromXPosition(curXPosition)) 30 | } 31 | 32 | function mouseMove (e) { 33 | if (!isMouseDown) return 34 | curXPosition = Math.max(e.offsetX, 0) 35 | setElapsed(getTimeFromXPosition(curXPosition)) 36 | } 37 | 38 | function mouseUp () { 39 | if (!isMouseDown) return 40 | isMouseDown = false 41 | } 42 | 43 | function getTimeFromXPosition (xPosition) { 44 | return xPosition / width * vizDuration 45 | } 46 | 47 | return function renderTimeline (elapsed, settings) { 48 | if (isMouseDown) { 49 | elapsed = getTimeFromXPosition(curXPosition) 50 | setElapsed(elapsed) 51 | } 52 | updateScrubberPosition(elapsed) 53 | updateHistogram(getBucket(settings)) 54 | } 55 | } 56 | 57 | function getHistogramBucketsByFilter (trips, granularity, vizDuration) { 58 | const bucketsByFilter = { 59 | subscriber: [], 60 | nonSubscriber: [] 61 | } 62 | const maxBuckets = vizDuration / granularity 63 | for (let trip of trips) { 64 | // +1 to every bucket that this trip touches 65 | const startBucket = getSeconds(trip.start_ts) / granularity | 0 66 | const endBucket = Math.min(maxBuckets, Math.ceil(trip.duration / granularity) + startBucket) 67 | const filter = trip.subscriber ? 'subscriber' : 'nonSubscriber' 68 | const buckets = bucketsByFilter[filter] 69 | for (let i = startBucket; i <= endBucket; i++) { 70 | buckets[i] = buckets[i] || 0 71 | buckets[i] += 1 72 | } 73 | } 74 | 75 | return (settings) => { 76 | const buckets = newArray(maxBuckets).map(() => 0) 77 | for (let filter in bucketsByFilter) { 78 | if (!settings[filter]) continue 79 | for (let i = 0; i < buckets.length; i++) { 80 | buckets[i] += bucketsByFilter[filter][i] 81 | } 82 | } 83 | return buckets 84 | } 85 | } 86 | 87 | function createSVG (el) { 88 | const { width, height } = el.getBoundingClientRect() 89 | const svgEl = el.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) 90 | svgEl.setAttribute('width', width) 91 | svgEl.setAttribute('height', height) 92 | css(svgEl, { position: 'absolute', top: 0, left: 0 }) 93 | return svgEl 94 | } 95 | 96 | function createScrubber (svgEl, vizDuration) { 97 | const { width, height } = svgEl.getBoundingClientRect() 98 | const lineEl = document.createElementNS('http://www.w3.org/2000/svg', 'line') 99 | lineEl.setAttribute('stroke', '#dbb685') 100 | lineEl.setAttribute('stroke-width', 2) 101 | lineEl.setAttribute('x1', 0) 102 | lineEl.setAttribute('y1', verticalPadding) 103 | lineEl.setAttribute('x2', 0) 104 | lineEl.setAttribute('y2', height - verticalPadding) 105 | svgEl.appendChild(lineEl) 106 | return function updatePosition (elapsed) { 107 | const x = elapsed / vizDuration * width 108 | lineEl.setAttribute('x1', x) 109 | lineEl.setAttribute('x2', x) 110 | } 111 | } 112 | 113 | function drawTimeline (svgEl, vizDuration) { 114 | const { width, height } = svgEl.getBoundingClientRect() 115 | const tickSize = 3 * 60 * 60 116 | const tickCount = vizDuration / tickSize // not including last tick 117 | const startY = height - verticalPadding + 10 // 10 is the max tick height 118 | for (let i = 0; i <= tickCount; i++) { 119 | const x = i / tickCount * width 120 | const lineLength = i === tickCount / 2 ? 10 : i === tickCount / 4 ? 7 : 5 121 | const lineEl = document.createElementNS('http://www.w3.org/2000/svg', 'line') 122 | lineEl.setAttribute('stroke', '#ddd') 123 | lineEl.setAttribute('x1', x) 124 | lineEl.setAttribute('y1', startY) 125 | lineEl.setAttribute('x2', x) 126 | lineEl.setAttribute('y2', startY + lineLength) 127 | svgEl.appendChild(lineEl) 128 | 129 | const timeEl = document.createElement('span') 130 | const time = secondsToTime(i / tickCount * vizDuration) 131 | const hour = convertHourFrom24H(time).split(':')[0] 132 | const meridian = getMeridian(time) 133 | timeEl.innerText = `${hour}${meridian}` 134 | const elWidth = 50 135 | css(timeEl, { 136 | position: 'absolute', 137 | top: height, 138 | left: x - elWidth / 2, 139 | width: elWidth, 140 | fontSize: 12, 141 | textAlign: 'center' 142 | }) 143 | svgEl.parentElement.appendChild(timeEl) 144 | } 145 | const lineEl = document.createElementNS('http://www.w3.org/2000/svg', 'line') 146 | lineEl.setAttribute('stroke', '#ddd') 147 | lineEl.setAttribute('x1', 0) 148 | lineEl.setAttribute('y1', startY - 1) 149 | lineEl.setAttribute('x2', width) 150 | lineEl.setAttribute('y2', startY - 1) 151 | svgEl.appendChild(lineEl) 152 | } 153 | 154 | function drawHistogram (svgEl, buckets) { 155 | const { width, height } = svgEl.getBoundingClientRect() 156 | const max = buckets.reduce((curMax, val) => Math.max(curMax, val), -Infinity) 157 | const histogramHeight = height - verticalPadding * 2 158 | 159 | const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path') 160 | pathEl.setAttribute('fill', 'rgba(255, 255, 255, 0.6)') 161 | pathEl.setAttribute('stroke', '#ddd') 162 | svgEl.appendChild(pathEl) 163 | 164 | const controlsSprings = buckets.map(() => createSpring(0.1, 1.1, 0)) 165 | 166 | updateHistogram(buckets) 167 | 168 | function getDAttribute (points) { 169 | let d = '' 170 | d += `M0,${histogramHeight + verticalPadding} ` 171 | points.forEach(p => { 172 | d += `L${p[0]},${p[1]} ` 173 | }) 174 | d += `L${points[points.length - 1][0]},${histogramHeight + verticalPadding}` 175 | return d 176 | } 177 | 178 | function updateHistogram (buckets) { 179 | controlsSprings.forEach((spring, i) => spring.updateValue(buckets[i])) 180 | const controls = controlsSprings.map(spring => spring.tick()).map((v, i) => [ 181 | i / (buckets.length - 1) * width, 182 | (histogramHeight + verticalPadding) - (v / max * histogramHeight) 183 | ]) 184 | controls.unshift(controls[0].map(v => v + 1)) 185 | controls.push(controls[controls.length - 1].map(v => v + 1)) 186 | const points = catRomSpline(controls, { samples: 2 }) 187 | pathEl.setAttribute('d', getDAttribute(points)) 188 | } 189 | 190 | return updateHistogram 191 | } 192 | -------------------------------------------------------------------------------- /src/create-trip-paths-renderer.js: -------------------------------------------------------------------------------- 1 | const glslify = require('glslify') 2 | const vec2 = require('gl-vec2') 3 | const lerp = require('lerp') 4 | const { getSeconds } = require('./helpers') 5 | 6 | module.exports = function createTripPathsRenderer (regl, points) { 7 | const startColor = [235, 127, 0] 8 | const endColor = [172, 240, 242] 9 | function getColor (t) { 10 | return [ 11 | lerp(startColor[0], endColor[0], t) / 255, 12 | lerp(startColor[1], endColor[1], t) / 255, 13 | lerp(startColor[2], endColor[2], t) / 255 14 | ] 15 | } 16 | 17 | function getPosition (start, end, t) { 18 | const position = vec2.lerp([], start, end, t) 19 | const z = Math.sin(t * Math.PI) * -0.1 * Math.max(0.04, vec2.distance(start, end)) 20 | position.push(z) 21 | return position 22 | } 23 | 24 | // because drawing in lines mode takes a start point, then an end point, 25 | // then the next line's start, and then end - we have to reorg these points 26 | // to work with the shader 27 | const linesPoints = [] 28 | const linesColors = [] 29 | const linesStartTimes = [] 30 | const linesDurations = [] 31 | const linesTripStateIndex = [] 32 | points.forEach(p => { 33 | const arcPoints = 25 34 | const startTime = getSeconds(p.start_ts) 35 | for (let j = 0; j < arcPoints; j++) { 36 | linesPoints.push(getPosition(p.startPosition, p.endPosition, j / arcPoints)) 37 | linesColors.push(getColor(j / arcPoints)) 38 | linesStartTimes.push(startTime) 39 | linesDurations.push(p.duration) 40 | linesTripStateIndex.push(p.tripStateIndex) 41 | 42 | linesPoints.push(getPosition(p.startPosition, p.endPosition, (j + 1) / arcPoints)) 43 | linesColors.push(getColor((j + 1) / arcPoints)) 44 | linesStartTimes.push(startTime) 45 | linesDurations.push(p.duration) 46 | linesTripStateIndex.push(p.tripStateIndex) 47 | } 48 | }) 49 | 50 | console.log('line segment points', linesPoints.length) 51 | 52 | return regl({ 53 | vert: glslify.file('./trip-path.vert'), 54 | frag: glslify.file('./trip-path.frag'), 55 | 56 | attributes: { 57 | position: linesPoints, 58 | color: linesColors, 59 | startTime: linesStartTimes, 60 | duration: linesDurations, 61 | tripStateIndex: linesTripStateIndex 62 | }, 63 | 64 | count: linesPoints.length, 65 | 66 | primitive: 'lines' 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/create-trip-points-renderer.js: -------------------------------------------------------------------------------- 1 | const glslify = require('glslify') 2 | const { getSeconds } = require('./helpers') 3 | 4 | module.exports = function createTripPointsRenderer (regl, points) { 5 | return regl({ 6 | vert: glslify.file('./trip-points.vert'), 7 | frag: glslify.file('./trip-points.frag'), 8 | 9 | attributes: { 10 | startPosition: points.map(p => p.startPosition), 11 | endPosition: points.map(p => p.endPosition), 12 | color: points.map(p => p.subscriber ? [0.7, 0.7, 1, 1] : [1, 0.7, 0.7, 1]), 13 | startTime: points.map(p => getSeconds(p.start_ts)), 14 | duration: points.map(p => p.duration), 15 | tripStateIndex: points.map(p => p.tripStateIndex) 16 | }, 17 | 18 | count: points.length, 19 | 20 | primitive: 'point' 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const mat4 = require('gl-mat4') 2 | const intersect = require('ray-plane-intersection') 3 | const pickRay = require('camera-picking-ray') 4 | 5 | module.exports = { 6 | extent, 7 | parseLines, 8 | parseTripsCSV, 9 | parseStationsCSV, 10 | mungeStationsIntoTrip, 11 | getSeconds, 12 | getMeridian, 13 | convertHourFrom24H, 14 | secondsToTime, 15 | getIntersection 16 | } 17 | 18 | function extent (lines) { 19 | let minLon, minLat, maxLon, maxLat 20 | for (let points of lines) { 21 | for (let pt of points) { 22 | if (!minLon || pt[0] < minLon) minLon = pt[0] 23 | if (!maxLon || pt[0] > maxLon) maxLon = pt[0] 24 | if (!minLat || pt[1] < minLat) minLat = pt[1] 25 | if (!maxLat || pt[1] > maxLat) maxLat = pt[1] 26 | } 27 | } 28 | return [minLon, minLat, maxLon, maxLat] 29 | } 30 | 31 | function parseLines (data) { 32 | return data 33 | .split('\n') 34 | .map(l => l.split(';') 35 | .map(pts => pts.split(',') 36 | .map(v => parseFloat(v)).filter(v => v) 37 | ).filter(l => l.length) 38 | ).filter(l => l.length) 39 | } 40 | 41 | function parseCSV (data, formatValue) { 42 | const lines = data.split('\n') 43 | const header = lines[0] 44 | const columns = header.split(',') 45 | return lines.slice(1).filter(l => l).map(line => { 46 | const obj = {} 47 | line.split(',').forEach((value, i) => { 48 | const name = columns[i] 49 | obj[name] = formatValue(value, name) 50 | }) 51 | return obj 52 | }) 53 | } 54 | 55 | function parseTripsCSV (data) { 56 | return parseCSV(data, (value, name) => { 57 | if (['birth_year', 'duration', 'end_station', 'start_station'].includes(name)) { 58 | return parseInt(value, 10) 59 | } 60 | if (name === 'subscriber') return (value === '1') 61 | return value 62 | }) 63 | } 64 | 65 | function parseStationsCSV (data) { 66 | const stations = {} 67 | parseCSV(data, (value, name) => { 68 | if (['latitude', 'longitude'].includes(name)) { 69 | return parseFloat(value) 70 | } 71 | if (name === 'station_id') return parseInt(value) 72 | // strip out the opening and closing quotes 73 | if (name === 'name') return value.slice(1, value.length - 1) 74 | return value 75 | }).forEach(station => { 76 | stations[station.station_id] = station 77 | }) 78 | return stations 79 | } 80 | 81 | function mungeStationsIntoTrip (trips, stations) { 82 | const filteredTrips = [] 83 | const knownMissingStations = {} 84 | for (let trip of trips) { 85 | const startStation = stations[trip.start_station] 86 | const endStation = stations[trip.end_station] 87 | if (!startStation) { 88 | if (!knownMissingStations[trip.start_station]) { 89 | console.warn(`station ${trip.start_station} not found in stations list`) 90 | } 91 | knownMissingStations[trip.start_station] = true 92 | continue 93 | } 94 | if (!endStation) { 95 | if (!knownMissingStations[trip.end_station]) { 96 | console.warn(`station ${trip.end_station} not found in stations list`) 97 | } 98 | knownMissingStations[trip.end_station] = true 99 | continue 100 | } 101 | trip.start_station = getLatLonFromStation(startStation) 102 | trip.end_station = getLatLonFromStation(endStation) 103 | filteredTrips.push(trip) 104 | } 105 | if (trips.length - filteredTrips.length > 0) { 106 | console.warn(`filtered out ${trips.length - filteredTrips.length} trips`) 107 | } 108 | return filteredTrips 109 | } 110 | 111 | function getLatLonFromStation (station) { 112 | const { latitude, longitude } = station 113 | return [longitude, latitude] 114 | } 115 | 116 | // gets seconds from day start 117 | function getSeconds (datetimeString) { 118 | const [, time] = datetimeString.split(' ') 119 | const [hours, minutes, seconds] = time.split(':').map(n => parseInt(n, 10)) 120 | return seconds + minutes * 60 + hours * 3600 121 | } 122 | 123 | function secondsToTime (seconds) { 124 | seconds = seconds | 0 125 | const hours = seconds / 3600 | 0 126 | seconds = seconds % 3600 127 | const minutes = seconds / 60 | 0 128 | const time = [hours, leftPad(minutes, 2, 0)].join(':') 129 | return `${time}` 130 | } 131 | 132 | function getMeridian (time) { 133 | const hours = parseInt(time.split(':')[0], 10) % 24 134 | return hours > 11 ? 'PM' : 'AM' 135 | } 136 | 137 | function convertHourFrom24H (time) { 138 | let [hours, minutes] = time.split(':') 139 | hours = parseInt(hours, 10) 140 | if (hours === 0) return `12:${minutes}` 141 | if (hours > 12) return `${hours - 12}:${minutes}` 142 | return time 143 | } 144 | 145 | function leftPad (val, len, char) { 146 | val = `${val}` 147 | while (val.length < len) { 148 | val = `${char}${val}` 149 | } 150 | return val 151 | } 152 | 153 | function getIntersection (mouse, viewport, projection, view) { 154 | const projView = mat4.multiply([], projection, view) 155 | const invProjView = mat4.invert([], projView) 156 | const rayOrigin = [] 157 | const rayDir = [] 158 | pickRay(rayOrigin, rayDir, mouse, viewport, invProjView) 159 | const normal = [0, 0, -1] 160 | const distance = 0 161 | return intersect([], rayOrigin, rayDir, normal, distance) 162 | } 163 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* global fetch */ 2 | 3 | const createRegl = require('regl') 4 | const fit = require('canvas-fit') 5 | const css = require('dom-css') 6 | const mat4 = require('gl-mat4') 7 | const createProjection = require('./create-projection') 8 | const createTripPointsRenderer = require('./create-trip-points-renderer') 9 | const createTripPathsRenderer = require('./create-trip-paths-renderer') 10 | const createStateTransitioner = require('./create-state-transitioner') 11 | const createMapRenderer = require('./create-map-renderer') 12 | const createElapsedTimeView = require('./create-elapsed-time-view') 13 | const createTimeline = require('./create-timeline') 14 | const createButtons = require('./create-buttons') 15 | const createRoamingCamera = require('./create-roaming-camera') 16 | const checkSupport = require('./check-support') 17 | const setupDatGUI = require('./setup-dat-gui') 18 | const { 19 | extent, 20 | parseLines, 21 | parseTripsCSV, 22 | parseStationsCSV, 23 | mungeStationsIntoTrip 24 | } = require('./helpers') 25 | 26 | const vizDuration = 24 * 60 * 60 27 | const BG_COLOR = [0.22, 0.22, 0.22, 1] 28 | const rgba = `rgba(${BG_COLOR.slice(0, 3).map(v => v * 256 | 0).join(',')}, ${BG_COLOR[3]})` 29 | css(document.body, 'background-color', rgba) 30 | 31 | const appContainer = document.querySelector('.app-container') 32 | const canvas = appContainer.querySelector('.visualization').appendChild(document.createElement('canvas')) 33 | const regl = createRegl({ 34 | extensions: 'OES_texture_float', 35 | canvas: canvas 36 | }) 37 | 38 | checkSupport(regl) 39 | 40 | window.addEventListener('resize', fit(canvas), false) 41 | 42 | const nycStreetsFile = './cleaned/nyc-streets.txt' 43 | const tripsFile = './cleaned/trips-2017-06-09.csv' 44 | const stationsFile = './cleaned/stations.csv' 45 | 46 | Promise.all([ 47 | fetch(nycStreetsFile).then(d => d.text()).then(parseLines), 48 | fetch(tripsFile).then(d => d.text()).then(parseTripsCSV), 49 | fetch(stationsFile).then(d => d.text()).then(parseStationsCSV) 50 | ]).then(function onLoad ([coordinates, trips, stations]) { 51 | trips = mungeStationsIntoTrip(trips, stations) 52 | 53 | const projectCoords = createProjection({ bbox: extent(coordinates), zoom: 1300 }) 54 | const lines = coordinates.map(points => points.map(projectCoords)) 55 | 56 | const getProjection = () => mat4.perspective( 57 | [], 58 | Math.PI / 4, 59 | window.innerWidth / window.innerHeight, 60 | 0.01, 61 | 10 62 | ) 63 | const focus = projectCoords([-74.006861, 40.724130]) // holland tunnel 64 | const [fX, fY] = focus 65 | const center = [fX - 0.15, fY + 0.15, -0.2] // i feel like these are misnamed in the 3d controls lib 66 | const eye = [fX, fY, 0] // i feel like these are misnamed in the 3d controls lib 67 | const camera = createRoamingCamera(canvas, focus, center, eye, getProjection) 68 | 69 | let settings = setupDatGUI({ 70 | speed: [60 * 15, 1, 7000, 1], 71 | rayPickerThreshold: [0.05, 0.01, 0.1, 0.01], 72 | startRoaming: camera.startRoaming, 73 | 'start/stop': () => { paused = !paused } 74 | }) 75 | 76 | settings = Object.assign(settings, { 77 | showPoints: true, 78 | showPaths: true, 79 | curvedPaths: true, 80 | subscriber: true, 81 | nonSubscriber: true 82 | }) 83 | 84 | let elapsed = 4 * 60 * 60 // start at 4:00 so we get into the action a lil' faster 85 | const setElapsed = (newElapsed) => { elapsed = newElapsed } 86 | const timelineEl = document.querySelector('.timeline') 87 | const renderTimeline = createTimeline(timelineEl, trips, vizDuration, setElapsed, settings) 88 | const renderElapsedTime = createElapsedTimeView(document.querySelector('.clock'), trips) 89 | const renderButtons = createButtons(document.querySelector('.buttons'), settings) 90 | 91 | for (let j = 0; j < trips.length; j++) { 92 | const trip = trips[j] 93 | trip.startPosition = projectCoords(trip.start_station) 94 | trip.endPosition = projectCoords(trip.end_station) 95 | } 96 | 97 | const stateTransitioner = createStateTransitioner(regl, trips, settings) 98 | const stateIndexes = stateTransitioner.getStateIndexes() 99 | 100 | for (let j = 0; j < trips.length; j++) { 101 | const trip = trips[j] 102 | trip.tripStateIndex = stateIndexes[j] 103 | } 104 | 105 | const drawTripPoints = createTripPointsRenderer(regl, trips) 106 | const drawTripPaths = createTripPathsRenderer(regl, trips) 107 | const renderMap = createMapRenderer(regl, lines) 108 | 109 | const globalRender = regl({ 110 | uniforms: { 111 | projection: regl.prop('projection'), 112 | view: regl.prop('view'), 113 | elapsed: regl.prop('elapsed'), 114 | center: regl.prop('center'), 115 | tripStateTexture: () => stateTransitioner.getStateTexture() 116 | }, 117 | blend: { 118 | enable: true, 119 | func: { 120 | srcRGB: 'src alpha', 121 | dstRGB: 1, 122 | srcAlpha: 1, 123 | dstAlpha: 1 124 | }, 125 | equation: { 126 | rgb: 'add', 127 | alpha: 'add' 128 | } 129 | } 130 | }) 131 | 132 | removeLoader() 133 | appContainer.classList.remove('hidden') 134 | let lastTime = 0 135 | let paused = false 136 | regl.frame(({ time }) => { 137 | if (!paused) { 138 | const timeDiff = (time - lastTime) * settings.speed 139 | elapsed = (elapsed + timeDiff) % vizDuration 140 | } 141 | lastTime = time 142 | 143 | regl.clear({ 144 | color: BG_COLOR, 145 | depth: 1 146 | }) 147 | 148 | const view = camera.getMatrix() 149 | 150 | camera.tick(settings) 151 | 152 | stateTransitioner.tick(Object.assign({ 153 | projection: getProjection(), 154 | view: view, 155 | viewport: [0, 0, window.innerWidth, window.innerHeight] 156 | }, settings)) 157 | 158 | renderButtons(settings) 159 | renderTimeline(elapsed, settings) 160 | renderElapsedTime(elapsed) 161 | globalRender({ 162 | elapsed: elapsed, 163 | center: camera.getCenter(), 164 | view: view, 165 | projection: getProjection() 166 | }, () => { 167 | renderMap() 168 | drawTripPoints() 169 | drawTripPaths() 170 | }) 171 | }) 172 | }) 173 | 174 | function removeLoader () { 175 | const loader = document.querySelector('.loader') 176 | loader.parentElement.removeChild(loader) 177 | } 178 | -------------------------------------------------------------------------------- /src/map.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 position; 2 | 3 | varying vec4 fragColor; 4 | 5 | uniform mat4 projection; 6 | uniform mat4 view; 7 | uniform vec3 color; 8 | uniform vec3 center; 9 | 10 | void main() { 11 | float d = distance(position, center.xy); 12 | float alpha = (1.0 - smoothstep(0.1, 0.8, d)) / 1.5; 13 | fragColor = vec4(color, alpha); 14 | gl_Position = projection * view * vec4(position, 0.0, 1.0); 15 | } 16 | -------------------------------------------------------------------------------- /src/pointer-circle.vert: -------------------------------------------------------------------------------- 1 | attribute float rads; 2 | 3 | varying vec4 fragColor; 4 | 5 | uniform vec3 center; 6 | uniform float size; 7 | uniform mat4 projection; 8 | uniform mat4 view; 9 | uniform vec3 color; 10 | 11 | void main() { 12 | vec3 position = vec3(cos(rads), sin(rads), 0.0) * size + center; 13 | fragColor = vec4(color, 0.8); 14 | gl_Position = projection * view * vec4(position.xy, 0.001, 1.0); 15 | } 16 | -------------------------------------------------------------------------------- /src/setup-dat-gui.js: -------------------------------------------------------------------------------- 1 | const { GUI } = require('dat-gui') 2 | 3 | module.exports = function guiSettings (settings) { 4 | const settingsObj = {} 5 | const gui = new GUI() 6 | gui.closed = true 7 | for (let key in settings) { 8 | if (typeof settings[key] === 'function') { 9 | gui.add({ [key]: settings[key] }, key) 10 | continue 11 | } 12 | settingsObj[key] = settings[key][0] 13 | const setting = gui 14 | .add(settingsObj, key, settings[key][1], settings[key][2]) 15 | if (settings[key][3]) { 16 | setting.step(settings[key][3]) 17 | } 18 | } 19 | return settingsObj 20 | } 21 | -------------------------------------------------------------------------------- /src/simple.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec4 fragColor; 4 | 5 | void main() { 6 | gl_FragColor = fragColor; 7 | } 8 | -------------------------------------------------------------------------------- /src/simple.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | 3 | varying vec4 fragColor; 4 | 5 | uniform mat4 projection; 6 | uniform mat4 view; 7 | uniform vec4 color; 8 | 9 | void main() { 10 | fragColor = color; 11 | gl_Position = projection * view * vec4(position, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /src/trip-path.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec4 fragColor; 4 | varying float discardMe; 5 | 6 | void main() { 7 | if (fragColor[3] == 0.0 || discardMe == 1.0) { 8 | discard; 9 | } 10 | gl_FragColor = fragColor; 11 | } 12 | -------------------------------------------------------------------------------- /src/trip-path.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | attribute vec3 color; 3 | attribute float startTime; 4 | attribute float duration; 5 | attribute vec2 tripStateIndex; 6 | 7 | varying vec4 fragColor; 8 | varying float discardMe; 9 | 10 | uniform mat4 projection; 11 | uniform mat4 view; 12 | uniform float elapsed; 13 | uniform sampler2D tripStateTexture; 14 | 15 | void main() { 16 | vec4 thisTripState = texture2D(tripStateTexture, tripStateIndex); 17 | float arcHeight = thisTripState.x; 18 | float pathAlpha = thisTripState.y; 19 | float middle = startTime + duration / 2.0; 20 | float endTime = startTime + duration; 21 | float buf = 200.0; 22 | float t; 23 | if (elapsed < middle) { 24 | t = smoothstep(startTime - buf / 2.0, middle, elapsed); 25 | } else { 26 | t = 1.0 - smoothstep(middle, endTime + buf, elapsed); 27 | } 28 | float w = 1.0; 29 | discardMe = 0.0; 30 | float alpha = t / 1.3 * pathAlpha; 31 | if (t == 0.0) { 32 | w = 0.0; 33 | discardMe = 1.0; 34 | alpha = 0.0; 35 | } 36 | float z = position.z * arcHeight; 37 | fragColor = vec4(color.rgb, alpha); 38 | gl_Position = projection * view * vec4(position.xy, z, w); 39 | } 40 | -------------------------------------------------------------------------------- /src/trip-points.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec4 fragColor; 4 | 5 | void main() { 6 | gl_FragColor = fragColor; 7 | } 8 | -------------------------------------------------------------------------------- /src/trip-points.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 startPosition; 2 | attribute vec2 endPosition; 3 | attribute vec4 color; 4 | attribute float startTime; 5 | attribute float duration; 6 | attribute vec2 tripStateIndex; 7 | 8 | varying vec4 fragColor; 9 | 10 | uniform mat4 projection; 11 | uniform mat4 view; 12 | uniform float elapsed; 13 | 14 | uniform sampler2D tripStateTexture; 15 | 16 | void main() { 17 | vec4 thisTripState = texture2D(tripStateTexture, tripStateIndex); 18 | float arcHeight = thisTripState.x; 19 | float pointSize = thisTripState.z; 20 | fragColor = color; 21 | float endTime = startTime + duration; 22 | float t = smoothstep(startTime, endTime, elapsed); 23 | float z = arcHeight * sin(3.1415 * t) * max(distance(startPosition, endPosition), 0.04) * -0.1; 24 | vec3 newPosition = vec3(mix(startPosition, endPosition, t), z); 25 | float w = 1.0; 26 | if (t == 0.0 || t == 1.0) { 27 | w = 0.0; 28 | } 29 | gl_PointSize = pointSize; 30 | gl_Position = projection * view * vec4(newPosition, w); 31 | } 32 | -------------------------------------------------------------------------------- /src/trip-state.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D curTripStateTexture; 4 | uniform sampler2D prevTripStateTexture; 5 | uniform sampler2D tripMetaDataTexture; 6 | uniform vec3 rayPicker; 7 | uniform float rayPickerThreshold; 8 | uniform float tick; 9 | uniform float dampening; 10 | uniform float stiffness; 11 | uniform float maxArcHeight; 12 | uniform float maxPointSize; 13 | uniform bool showSubscriber; 14 | uniform bool showNonSubscriber; 15 | uniform bool curvedPaths; 16 | uniform bool showPaths; 17 | uniform bool showPoints; 18 | 19 | varying vec2 tripStateIndex; 20 | 21 | float getNextValue(float cur, float prev, float dest) { 22 | float velocity = cur - prev; 23 | float delta = dest - cur; 24 | float spring = delta * stiffness; 25 | float damper = velocity * -1.0 * dampening; 26 | return spring + damper + velocity + cur; 27 | } 28 | 29 | void main() { 30 | vec4 curState = texture2D(curTripStateTexture, tripStateIndex); 31 | vec4 prevState = texture2D(prevTripStateTexture, tripStateIndex); 32 | vec4 tripMetaData = texture2D(tripMetaDataTexture, tripStateIndex); 33 | 34 | bool isSubscriber = tripMetaData.x == 1.0; 35 | vec2 tripStart = tripMetaData.yz; 36 | 37 | bool isNearRayPicker = distance(tripStart, rayPicker.xy) < rayPickerThreshold; 38 | 39 | float destArcHeight = 0.0; 40 | float destPathAlpha = 0.0; 41 | float destPointSize = 0.0; 42 | 43 | if (isSubscriber) { 44 | if (isNearRayPicker && showSubscriber && curvedPaths) destArcHeight = maxArcHeight; 45 | if (isNearRayPicker && showSubscriber && showPaths) destPathAlpha = 1.0; 46 | if (isNearRayPicker && showSubscriber && showPoints) destPointSize = maxPointSize; 47 | } else { 48 | if (isNearRayPicker && showNonSubscriber && curvedPaths) destArcHeight = maxArcHeight; 49 | if (isNearRayPicker && showNonSubscriber && showPaths) destPathAlpha = 1.0; 50 | if (isNearRayPicker && showNonSubscriber && showPoints) destPointSize = maxPointSize; 51 | } 52 | 53 | float arcHeight = getNextValue(curState.x, prevState.x, destArcHeight); 54 | float pathAlpha = getNextValue(curState.y, prevState.y, destPathAlpha); 55 | float pointSize = getNextValue(curState.z, prevState.z, destPointSize); 56 | 57 | gl_FragColor = vec4(arcHeight, pathAlpha, pointSize, 0.0); 58 | } 59 | -------------------------------------------------------------------------------- /src/trip-state.vert: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | attribute vec2 position; 4 | 5 | varying vec2 tripStateIndex; 6 | 7 | void main() { 8 | // map bottom left -1,-1 (normalized device coords) to 0,0 (particle texture index) 9 | // and 1,1 (ndc) to 1,1 (texture) 10 | tripStateIndex = 0.5 * (1.0 + position); 11 | gl_Position = vec4(position, 0, 1); 12 | } 13 | --------------------------------------------------------------------------------