├── .gitignore ├── img ├── after.png └── before.png ├── README.md ├── css └── style.css └── nutrition.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /img/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenirby/myfitnesspal-reports/HEAD/img/after.png -------------------------------------------------------------------------------- /img/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenirby/myfitnesspal-reports/HEAD/img/before.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Myfitnesspal Report Bookmarklet 2 | ==================== 3 | 4 | * Author: Steven Irby [http://stevenirby.me/](http://stevenirby.me) 5 | * License: MIT 6 | * Meaning: Use everywhere and keep copyright header. 7 | 8 | Myfitnesspal Reports - Display pretty interactive charts for all your nutritional, excersice, and weight progress. 9 | 10 | ## Getting Started 11 | 12 | * Go [here](http://stevenirby.github.io/myfitnesspal-reports/) and drag the bookmarklet up to your browser bookmark toolbar. 13 | * Login to myfitnesspal.com 14 | * Go to any page and press the bookmarklet button on your toolbar. 15 | 16 | Myfitnesspal Report Bookmarklet turns this: 17 | 18 | ![before](https://github.com/stevenirby/myfitnesspal-reports/raw/master/img/before.png) 19 | 20 | Into this: 21 | 22 | ![after](https://github.com/stevenirby/myfitnesspal-reports/raw/master/img/after.png) 23 | 24 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | display: none; 3 | position: fixed; 4 | z-index: 1000; 5 | top: 0; 6 | left: 0; 7 | height: 100%; 8 | width: 100%; 9 | background: rgba( 255, 255, 255, .8 ) url('http://i.stack.imgur.com/FhHRx.gif') 50% 50% no-repeat; 10 | } 11 | 12 | body.showModal { 13 | overflow: hidden; 14 | } 15 | 16 | body.showModal .modal { 17 | display: block; 18 | } 19 | 20 | .selectionContainer { 21 | display: none; 22 | } 23 | 24 | .main { 25 | display: none; 26 | width: 100%; 27 | height: 150px; 28 | } 29 | 30 | .main .weight h4, 31 | .main .calories h4 { 32 | display: inline-block; 33 | } 34 | 35 | 36 | button.reportButton { 37 | display: none; 38 | -moz-box-shadow:inset 0px 1px 0px 0px #ffffff; 39 | -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff; 40 | box-shadow:inset 0px 1px 0px 0px #ffffff; 41 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf)); 42 | background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% ); 43 | background-color: #ededed; 44 | -moz-border-radius: 6px; 45 | -webkit-border-radius: 6px; 46 | border-radius: 6px; 47 | border: 1px solid #dcdcdc; 48 | color:#777777; 49 | font-family:arial; 50 | font-size:15px; 51 | font-weight:bold; 52 | padding:6px 24px; 53 | text-decoration: none; 54 | text-shadow: 1px 1px 0px #ffffff; 55 | } 56 | 57 | button.zoom{ 58 | display: block; 59 | -moz-box-shadow:inset 0px 1px 0px 0px #ffffff; 60 | -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff; 61 | box-shadow:inset 0px 1px 0px 0px #ffffff; 62 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf)); 63 | background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% ); 64 | background-color: #ededed; 65 | -moz-border-radius: 6px; 66 | -webkit-border-radius: 6px; 67 | border-radius: 6px; 68 | border: 1px solid #dcdcdc; 69 | color:#777777; 70 | font-family:arial; 71 | font-size:15px; 72 | font-weight:bold; 73 | padding:6px 24px; 74 | text-decoration: none; 75 | text-shadow: 1px 1px 0px #ffffff; 76 | } 77 | 78 | .masterGraph { 79 | position: relative; 80 | width: 600px; 81 | height: 100px; 82 | border: 1px solid #ddd; 83 | background: #fff; 84 | } 85 | 86 | .masterGraphDescription { 87 | text-align: center; 88 | width: 600px; 89 | } 90 | 91 | #slider-range { 92 | width: 566px; 93 | margin-left: 15px; 94 | } 95 | 96 | .graph { 97 | width: 600px; 98 | height: 400px; 99 | position: relative; 100 | box-sizing: border-box; 101 | padding: 20px 15px 15px 15px; 102 | margin: 0 auto 5px 0; 103 | border: 1px solid #ddd; 104 | background: #fff; 105 | background: linear-gradient(#f6f6f6 0, #fff 50px); 106 | background: -o-linear-gradient(#f6f6f6 0, #fff 50px); 107 | background: -ms-linear-gradient(#f6f6f6 0, #fff 50px); 108 | background: -moz-linear-gradient(#f6f6f6 0, #fff 50px); 109 | background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px); 110 | box-shadow: 0 3px 10px rgba(0,0,0,0.15); 111 | -o-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 112 | -ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 113 | -moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 114 | -webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 115 | } 116 | 117 | .graphContainer .zoomContainer { 118 | height: 50px; 119 | display: none; 120 | } 121 | 122 | .graphContainer button { 123 | float: left; 124 | } 125 | 126 | .graphContainer .zoomContainer a { 127 | padding-left: 10px; 128 | right: 0; 129 | } 130 | 131 | .legend { 132 | width: 200px; 133 | } 134 | 135 | /*========== Subsection comment block ==========*/ 136 | { 137 | width: 0px; 138 | height: 0px; 139 | border-left: 7px solid transparent; 140 | border-right: 7px solid transparent; 141 | font-size:0px; 142 | line-height:0px; 143 | top:-2px; 144 | position:relative; 145 | display:inline-block; 146 | } 147 | .down.green { 148 | border-top: 7px solid green; 149 | } 150 | .up.green { 151 | border-bottom: 7px solid green; 152 | } 153 | 154 | .down.red { 155 | border-top: 7px solid red; 156 | text-align: center; 157 | } 158 | .up.red { 159 | border-bottom: 7px solid red; 160 | line-height: 2px; 161 | } -------------------------------------------------------------------------------- /nutrition.js: -------------------------------------------------------------------------------- 1 | /** 2 | Myfitnesspal Reports Bookmarklet Copyright 2013 Steven Irby 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | * 23 | * @file 24 | * @author Steven Irby 25 | * @email "Steven Irby" [info@stevenirby.me] 26 | * @email "Moises Romero" [ezzygemini@gmail.com] 27 | * @since 2013 28 | * @version 2 29 | * @copyright Copyright 2013 Steven Irby 30 | */ 31 | // TODO - re-write time! 32 | // - if something fails to download, try again for 2 more times, then give up, 33 | // and show message saying, sorry didn't download 34 | // - re-write so everything is asyncronous, so one data is downloaded, graph 35 | // it. No waiting around for all the data to download. Lame. 36 | // - add new default to drop-down "this week" starting from Monday. 37 | (function () { 38 | 39 | /** 40 | * add method to Date for adding days 41 | * @memberof Date.prototype 42 | * @param days 43 | * @returns {Date} 44 | */ 45 | Date.prototype.addDays = function(days) { 46 | var date = new Date(this.valueOf()) 47 | date.setDate(date.getDate() + days); 48 | return date; 49 | }; 50 | 51 | /** 52 | * @memberof Date.prototype 53 | * @param days 54 | * @returns {Date} 55 | */ 56 | Date.prototype.removeDays = function(days) { 57 | var date = new Date(this.valueOf()) 58 | date.setDate(date.getDate() - days); 59 | return date; 60 | }; 61 | 62 | /** 63 | * add script to the page 64 | * @param src 65 | * @param cb 66 | */ 67 | function addScript(src, cb) { 68 | var script = document.createElement('script'); 69 | script.src = src; 70 | document.documentElement.appendChild(script); 71 | script.onload = function() { 72 | if (typeof(cb) === 'function') { 73 | cb(); 74 | } 75 | }; 76 | } 77 | 78 | /** 79 | * add script to the page 80 | * @param src 81 | * @param cb 82 | */ 83 | function addLink(src, cb) { 84 | var link = document.createElement('link'); 85 | link.href = src; 86 | link.type = "text/css"; 87 | link.rel = "stylesheet"; 88 | document.getElementsByTagName('head')[0].appendChild(link); 89 | } 90 | 91 | /** 92 | * Main Report Class 93 | * @constructor 94 | */ 95 | function Report() { 96 | 97 | /** 98 | * Init script 99 | */ 100 | this.init = function(){ 101 | this.days = 364; 102 | this.dates = []; 103 | this.allGraphs = []; 104 | 105 | this.segments = { 106 | nutrition: [ 107 | 'Net Calories', 108 | 'Calories', 109 | 'Carbs', 110 | 'Fat', 111 | 'Protein', 112 | 'Saturated Fat', 113 | 'Polyunsaturated Fat', 114 | 'Monounsaturated Fat', 115 | 'Trans Fat', 116 | 'Cholesterol', 117 | 'Sodium', 118 | 'Potassium', 119 | 'Fiber', 120 | 'Sugar', 121 | 'Vitamin A', 122 | 'Vitamin C', 123 | 'Iron', 124 | 'Calcium' 125 | ], 126 | fitness: [ 127 | 'Calories Burned', 128 | 'Exercise Minutes' 129 | ], 130 | progress: [ 131 | '1' 132 | ] 133 | }; 134 | 135 | this.dfds = []; 136 | 137 | // add modal markup to page 138 | var modal = [ 139 | '' 141 | ], 142 | markup = [ 143 | '
', 144 | '

Your Progress at a Glance

', 145 | '

Weight:

 ', 146 | ' What\'s this?', 147 | '
', 148 | '

Net Calorie Average so far this week:

 ', 149 | ' What\'s this?', 150 | '
', 151 | '
', 152 | '

' 153 | ], 154 | me = this; 155 | 156 | $('body').append($(modal.join(''))); 157 | 158 | 159 | this.cleanDom(); 160 | 161 | $('#content').append($(markup.join(''))); 162 | $( document ).tooltip(); 163 | 164 | this.showModal(); 165 | this.createDates(); 166 | this.generateData(); 167 | 168 | // wait for all the data before continuing on 169 | // TODO - what if there is an error? 170 | $.when.apply($, this.dfds).always(function () { 171 | me.setWeightTrend(); 172 | me.setCarloriesTrend(); 173 | me.addMasterGraph(); 174 | me.addGraphs(); 175 | me.hideModal(); 176 | me.zoomAllGraphs(); 177 | }); 178 | 179 | return this; 180 | }; 181 | 182 | this.showModal = function () { 183 | $('body').addClass('showModal'); 184 | }; 185 | 186 | this.hideModal = function () { 187 | $('body').removeClass('showModal'); 188 | $('.main').show(); 189 | }; 190 | 191 | /** 192 | * generate list of dates for number of days 193 | */ 194 | this.createDates = function () { 195 | var startDate, 196 | stopDate, 197 | currentDate, 198 | date = new Date(); 199 | 200 | date.setDate(date.getDate() - this.days); 201 | 202 | startDate = date; 203 | stopDate = new Date(); 204 | // set the dates to midnight, for better accuracy 205 | startDate.setHours(0,0,0,0); 206 | stopDate.setHours(0,0,0,0); 207 | 208 | currentDate = startDate; 209 | while (currentDate <= stopDate) { 210 | this.dates.push(currentDate.getTime()); 211 | currentDate = currentDate.addDays(1); 212 | } 213 | }; 214 | 215 | /** 216 | * generate data for graphs 217 | */ 218 | this.generateData = function () { 219 | this.allData = {}; 220 | var i, fields, 221 | x = 0, f = 0, field, key, 222 | me = this, n; 223 | 224 | // TODO - save data to local storage, if there is no new data to fetch.... 225 | // - not sure how to know if there is or isn't data to fetch, maybe if script is ran, within an hour of last being ran 226 | // don't pull new data in? 227 | // - maybe if local storage is used, I could add a message somewhere that says, clear cache or something.... 228 | 229 | // iterate over segments nutrition, fitness, "1" (really why are they using "1"!?) 230 | for (key in this.segments) { 231 | // iterate over the fields 232 | if (this.segments.hasOwnProperty(key)) { 233 | fields = this.segments[key]; 234 | // run through the fields (calories, sugar, fiber, etc.) 235 | for (f = 0; f < fields.length; f++) { 236 | me.allData[fields[f]] = []; 237 | // push the chained function into a list of deferreds, so we can wait for them 238 | // all to finsh. Of course, pass in the correct reffernces 239 | me.dfds.push(me.fetchData(key, fields[f]).done($.proxy(function (fields, f, json) { 240 | var data = json.data, value, 241 | text = json.label; 242 | $('.modal h3 span').text(text); 243 | // get dates from first row string, and only do this once! 244 | for (n = 0; n < me.dates.length; n++) { 245 | if(!data[n] || data[n].total === undefined) 246 | continue; 247 | value = data[n].total; 248 | me.allData[fields[f]].push([me.dates[n], value]); 249 | } 250 | }, this, fields, f))); 251 | } 252 | } 253 | } 254 | }; 255 | 256 | /** 257 | * asynchronously request xml from myfitnesspal 258 | * @param segment 259 | * @param field 260 | * @returns {*} 261 | */ 262 | this.fetchData = function (segment, field) { 263 | var url = 'https://www.myfitnesspal.com/reports/results/'; 264 | url = url + segment + '/' + field + '/365.json'; // set this to 365 - weight loss data only comes in 7, 30, 90, and 365 265 | 266 | return $.ajax({ 267 | type: 'GET', 268 | url: url, 269 | dataType: "json", 270 | success: function (json){ 271 | return json; 272 | } 273 | }).fail(function () { 274 | // TODO - retry? 275 | }); 276 | }; 277 | 278 | /** 279 | * clear the DOM of anything 280 | */ 281 | this.cleanDom = function () { 282 | $('#content').empty(); 283 | }; 284 | 285 | /** 286 | * set the trending weight 287 | */ 288 | this.setWeightTrend = function () { 289 | // first populate the progress part 290 | var weight = this.allData["1"].slice(-1)[0][1], 291 | lastWeight = 0, 292 | foundNumber = false, 293 | direction = 'down', 294 | color = 'green', 295 | i = this.allData["1"].length; 296 | 297 | // loop through years worth of weighins and find the last different one 298 | while (i-- && !foundNumber) { 299 | if (this.allData["1"][i][1] > 0 && this.allData["1"][i][1] !== weight) { 300 | lastWeight = this.allData["1"][i][1]; 301 | if (lastWeight < weight) { 302 | direction = 'up'; 303 | color = 'red'; 304 | } 305 | 306 | foundNumber = true; 307 | } 308 | } 309 | 310 | var $content = $('#content'), 311 | tooltip = 'Was: ' + lastWeight + ' Now: ' + weight; 312 | 313 | $content.find('.main .weight .weightNumber').text(weight); 314 | $content.find('.main .weight .weightNumber').parent().attr('title', tooltip); 315 | $content.find('.main .weight .arrow').addClass(direction).addClass(color); 316 | }; 317 | 318 | /** 319 | * set the calories trend: 320 | * - this looks at the current weeks average calorie count, 321 | * - against the all the previous weeks averages for the last month 322 | */ 323 | this.setCarloriesTrend = function () { 324 | var d = new Date(), 325 | day = d.getDay(), // get current day 0 - 6 326 | thisWeeksAverage, 327 | lastMonthAverage, 328 | direction = 'up', 329 | color = 'red'; 330 | 331 | // get this weeks average first 332 | if (day > 0) { 333 | thisWeeksAverage = this._getWeekAverage(d, d.removeDays(day)); 334 | d = d.removeDays(day); 335 | } else { 336 | // since it's sunday, we want the whole week 337 | thisWeeksAverage = this._getWeekAverage(d, d.removeDays(7)); 338 | d = d.removeDays(7); 339 | } 340 | 341 | // well just look at the last 28 days, so four weeks 342 | lastMonthAverage = this._getWeekAverage(d, d.removeDays(28)); 343 | 344 | var direction, color; 345 | if (lastMonthAverage > thisWeeksAverage) { 346 | direction = 'down'; 347 | color = 'green'; 348 | } 349 | 350 | $('#content').find('.main .calories .caloriesNumber').text(thisWeeksAverage); 351 | 352 | var tooltip = 'Was: ' + lastMonthAverage + ' Now: ' + thisWeeksAverage; 353 | $('#content').find('.main .calories .caloriesNumber').parent().attr('title', tooltip); 354 | $('#content').find('.main .calories .arrow').addClass(direction).addClass(color); 355 | }; 356 | 357 | /** 358 | * takes one or two date objects and returns the day for that range of dates 359 | * @param end 360 | * @param begin 361 | * @returns {number|*} 362 | * @private 363 | */ 364 | this._getWeekAverage = function (end, begin) { 365 | var arr = this.allData['Net Calories'], 366 | from = this.dates.indexOf(begin.setHours(0,0,0,0)), 367 | to = this.dates.indexOf(end.setHours(0,0,0,0)), 368 | data = arr.slice(from, to), 369 | dataLength = data.length, 370 | i, sum = 0, 371 | average, 372 | value; 373 | 374 | for (i = 0; i < dataLength; i++) { 375 | value = parseFloat(data[i][1], 10); 376 | sum += value; 377 | } 378 | 379 | average = Math.round(sum / dataLength); 380 | 381 | if (!isNaN(average)) { 382 | return average; 383 | } 384 | }; 385 | 386 | /** 387 | * Add the master graph which controls the zoom for all graphs 388 | */ 389 | this.addMasterGraph = function () { 390 | this.masterGraph = new MasterGraph().init(this); 391 | }; 392 | 393 | /** 394 | * create a new graph object for all fields 395 | */ 396 | this.addGraphs = function () { 397 | this.allGraphs.push( new LookbackGraph().init(this) ); 398 | 399 | var i, key, fields, fieldsLength; 400 | 401 | for (key in this.segments) { 402 | if (this.segments.hasOwnProperty(key)) { 403 | fields = this.segments[key]; 404 | fieldsLength = fields.length; 405 | 406 | for (i = 0; i < fieldsLength; i++) { 407 | if (fields[i] !== '1') { 408 | this.allGraphs.push(new SegmentGraph().init(this, fields[i])); 409 | } 410 | } 411 | } 412 | } 413 | }; 414 | 415 | /** 416 | * zoom all graphs to specified range 417 | */ 418 | this.zoomAllGraphs = function () { 419 | var i, 420 | range = this.range, 421 | graphsLength = this.allGraphs.length, 422 | graph; 423 | 424 | // loop though all graphs, and trigger the selected event, so graph 425 | // gets updated with new subset of data. 426 | for (i = 0; i < graphsLength; i++) { 427 | graph = this.allGraphs[i]; 428 | 429 | // turn zooming mode on so plotselected does everything 430 | graph.zooming = true; 431 | graph.$graph.trigger('plotselected', [range]); 432 | graph.zooming = false; 433 | } 434 | }; 435 | 436 | } 437 | 438 | 439 | /** 440 | * Base Graph 441 | * @constructor 442 | */ 443 | function Graph(){ 444 | 445 | /** 446 | * Initializes the graph 447 | * @returns {Graph} 448 | */ 449 | this.init = function(){ 450 | // needs to be overridden 451 | return this; 452 | }; 453 | 454 | /** 455 | * Renders the graph 456 | * @returns {Graph} 457 | */ 458 | this.graphData = function(){ 459 | // needs to be overridden 460 | return this; 461 | } 462 | } 463 | 464 | /** 465 | * master graph controls zooming for all graphs 466 | * @constructor 467 | * @extends {Graph} 468 | */ 469 | function MasterGraph(){ 470 | /** 471 | * Initializes the graph 472 | * @param parent 473 | * @returns {MasterGraph} 474 | */ 475 | this.init = function(parent){ 476 | var markup = [ 477 | '
', 478 | '
', 479 | '
', 480 | '
', 481 | '

Click and drag - to select a range for all graphs

', 482 | ' ', 502 | '
', 503 | '
' 504 | ]; 505 | 506 | this._parent = parent; 507 | this.daysShown = 7; // default for how many days to show when page loads 508 | this.$container = $(markup.join('')); 509 | this.chartOptions = { 510 | grid: { 511 | show: true, 512 | aboveData: false, 513 | axisMargin: 0, 514 | borderWidth: 0, 515 | clickable: false, 516 | hoverable: false, 517 | autoHighlight: false, 518 | mouseActiveRadius: 50 519 | }, 520 | xaxes: [ 521 | {mode: "time", labelWidth: 30} 522 | 523 | ], 524 | yaxes: [ 525 | {min: 0, show: false}, 526 | {show: false} 527 | ], 528 | series: {curvedLines: {active: true}}, 529 | selection: { 530 | mode: "x" 531 | }, 532 | legend: { 533 | show: false 534 | } 535 | }; 536 | 537 | this.graphData(); 538 | this.bindEvents(); 539 | 540 | return this; 541 | }; 542 | 543 | /** 544 | * graph the data 545 | */ 546 | this.graphData = function () { 547 | var field = 'Net Calories'; 548 | 549 | this.series = [{ 550 | data : this._parent.allData[field], 551 | yaxis: 1 552 | }, 553 | { 554 | data : this._parent.allData['1'], 555 | yaxis: 2 556 | } 557 | ]; 558 | 559 | $('#content').append(this.$container); 560 | this.$graph = this.$container.find('.masterGraph'); 561 | this.plot = $.plot(this.$graph, this.series, this.chartOptions); 562 | this.makeSelection(); 563 | this.$container.find('#daySelect').val(this.daysShown); 564 | this.bindEvents(); 565 | }; 566 | 567 | /** 568 | * make a selection on the master graph for number of days shown 569 | */ 570 | this.makeSelection = function () { 571 | var xaxis = { 572 | xaxis: this._getDatesFromRange(this.daysShown) 573 | }; 574 | 575 | this.plot.setSelection(xaxis); 576 | this._parent.range = xaxis; // keep track of current data range for all graphs 577 | this._updateDateRange(); 578 | }; 579 | 580 | /** 581 | * update the range so user can see date range currently being shown 582 | * @private 583 | */ 584 | this._updateDateRange = function () { 585 | var _from = new Date(this._parent.range.xaxis.from).setHours(0,0,0,0), 586 | _to = new Date(this._parent.range.xaxis.to).setHours(0,0,0,0), 587 | from = new Date(_from).toDateString(), 588 | to = new Date(_to).toDateString(), 589 | str = 'Selected Dates: ' + from + ' - ' + to; 590 | 591 | this.$container.find('.dateRange').text(str); 592 | }; 593 | 594 | /** 595 | * take a number of days, and return the two dates 596 | * @param days 597 | * @returns {{from, to}} 598 | * @private 599 | */ 600 | this._getDatesFromRange = function (days) { 601 | var now = new Date(), 602 | d = new Date(), 603 | day = d.getDay(), 604 | from = d.removeDays(days).setHours(0,0,0,0), 605 | to = now.setHours(0,0,0,0); 606 | 607 | // if this is set to months, then get that date instead 608 | if (('' + days).indexOf('m') > -1) { 609 | from = d.setMonth(d.getMonth() - parseInt(days.replace(/m/g, ''), 10)); 610 | from = new Date(from).setHours(0,0,0,0); 611 | } 612 | 613 | return {from: from, to: to}; 614 | }; 615 | 616 | /** 617 | * bind events for: 618 | * - selecting the master graph 619 | * - selecting a range from the drop down 620 | */ 621 | this.bindEvents = function () { 622 | var me = this; 623 | 624 | this.$container.find('#daySelect').unbind('change').bind('change', function () { 625 | var value = $(this).val(); 626 | 627 | if (value !== '-1') { 628 | // if not a month, use the value for the days 629 | me.dropdownChange = true; 630 | me.daysShown = value; 631 | me.makeSelection(); 632 | me._parent.zoomAllGraphs(); 633 | me.dropdownChange = false; 634 | } 635 | }); 636 | 637 | this.$graph.bind('plotselecting', function (event, ranges) { 638 | if ($.type(ranges) !== 'null') { 639 | me._parent.range = { 640 | xaxis: {from: ranges.xaxis.from, to: ranges.xaxis.to} 641 | } 642 | me._updateDateRange(); 643 | } 644 | }); 645 | 646 | this.$graph.bind('plotselected', function (event, ranges, dropdown) { 647 | if ($.type(ranges) !== 'null') { 648 | me._parent.range = { 649 | xaxis: {from: ranges.xaxis.from, to: ranges.xaxis.to} 650 | } 651 | me._parent.zoomAllGraphs(); 652 | me._updateDateRange(); 653 | 654 | // is this being fired because of the drop down change or a chart selection? 655 | // if it's not because of the drop down, then chance the drop down 656 | if (!me.dropdownChange) { 657 | me.$container.find('#daySelect').val(0); 658 | } 659 | } 660 | }); 661 | }; 662 | } 663 | MasterGraph.prototype = new Graph(); 664 | MasterGraph.prototype.constructor = MasterGraph; 665 | 666 | 667 | 668 | /** 669 | * Segment Graph 670 | * @constructor 671 | * @extends {Graph} 672 | */ 673 | function SegmentGraph() { 674 | 675 | /** 676 | * Initializes the graph giving the ability to override the method 677 | * @param parent 678 | * @param field 679 | * @param {String} [opt_label] The optional label (hard-coded) 680 | * @param {Object} [opt_chartOptions] The chart options 681 | * @returns {SegmentGraph} 682 | */ 683 | this.init = function(parent, field, opt_label, opt_chartOptions){ 684 | var markup = [ 685 | '
', 686 | '

Average:

', 687 | '

You selected:

', 688 | '
What\'s this?
', 689 | '
What\'s this?
', 690 | '
What\'s this?
', 691 | '
What\'s this?
', 692 | '
', 693 | ' ', 694 | '
', 695 | '
', 696 | ]; 697 | 698 | this._parent = parent; // lazily pass in parent 699 | this.field = field; 700 | this.zooming = false; 701 | this.$container = $(markup.join('')); 702 | this.$container.find('h2').text(opt_label || field); 703 | this.setAverage(); 704 | this.previousHoverPoint = null; 705 | this.chartOptions = $.extend(true, { 706 | grid: { 707 | aboveData: false, 708 | axisMargin: 0, 709 | borderWidth: 0, 710 | clickable: true, 711 | hoverable: true, 712 | autoHighlight: true, 713 | mouseActiveRadius: 50 714 | }, 715 | xaxes: [ 716 | {mode: "time", labelWidth: 30}, 717 | ], 718 | yaxes: [ 719 | {min: 0}, 720 | {position: 'right', labelWidth: 30} 721 | ], 722 | series: {curvedLines: {active: true}}, 723 | selection: { 724 | mode: "x" 725 | }, 726 | legend: { 727 | show: true, 728 | position: 'nw', 729 | container: this.$container.find('.legend'), 730 | backgroundColor: null 731 | } 732 | }, opt_chartOptions); 733 | 734 | this.graphData(); 735 | 736 | return this; 737 | }; 738 | 739 | /** 740 | * get the average for the current range of data shown on graph 741 | * @param data 742 | * @returns {number|*} 743 | */ 744 | this.setAverage = function (data) { 745 | var data = data || this._parent.allData[this.field], 746 | dataLength = data.length, 747 | i, sum = 0, 748 | average, 749 | value; 750 | 751 | for (i = 0; i < dataLength; i++) { 752 | value = parseFloat(data[i][1], 10); 753 | sum += value; 754 | } 755 | 756 | average = Math.round(sum / dataLength); 757 | 758 | if (!isNaN(average)) { 759 | this.$container.find('h3 span').text(average); 760 | } 761 | 762 | return average 763 | }; 764 | 765 | /** 766 | * add graphs to the page 767 | * @param {Object[]} [opt_series] 768 | */ 769 | this.graphData = function (opt_series) { 770 | this.series = opt_series || [ 771 | { 772 | label: this.field, 773 | data : this._parent.allData[this.field], 774 | lines: { show: true, lineWidth: 3}, 775 | curvedLines: {apply:true}, 776 | yaxis: 1 777 | }, 778 | { 779 | label: 'Weight Loss', 780 | data : this._parent.allData['1'], 781 | lines: { show: true, lineWidth: 3}, 782 | curvedLines: {apply:true}, 783 | yaxis: 2 784 | } 785 | ]; 786 | 787 | if (!this._parent.allData[this.field].length) { 788 | var $msg = ' Opps! Failed to download this data. This happens because myfitnesspal took to long to send this data.'; 789 | this.$container.find('h2').after($msg); 790 | } 791 | $('#content').append(this.$container); 792 | this.$graph = this.$container.find('.graph'); 793 | this.plot = $.plot(this.$graph, this.series, this.chartOptions); 794 | 795 | this._fixUpLegend(); 796 | this.bindEvents(); 797 | }; 798 | 799 | /** 800 | * fix legend that breaks because of funky css on the page 801 | * @private 802 | */ 803 | this._fixUpLegend = function () { 804 | this.$container.find('table').css('width', 'auto'); 805 | this.$container.find('td').css({'border-bottom' : '0', 'vertical-align' : 'middle'}); 806 | this.$container.find('.legendLabel').css('padding-left', '10px'); 807 | }; 808 | 809 | /** 810 | * @param dateObj 811 | * @param backwards 812 | * @returns {string} 813 | */ 814 | this.convertDateToString = function (dateObj, backwards) { 815 | var d = new Date(parseInt(dateObj, 10)), 816 | month = d.getMonth() + 1, 817 | day = d.getDate(), 818 | year = d.getFullYear(), 819 | date = month + "-" + day + "-" + year; 820 | 821 | if (backwards) { 822 | date = year + "-" + month + "-" + day; 823 | } 824 | 825 | return date; 826 | }; 827 | 828 | /** 829 | * bind all graph events 830 | */ 831 | this.bindEvents = function () { 832 | var me = this; 833 | this.$container.find('.zoomContainer:eq(0)').show(); 834 | this.$container.find('button').bind('click', $.proxy(me._zoomButton, me)); 835 | this.$graph.bind('plothover', $.proxy(me._plotHover, me)); 836 | this.$graph.bind('plotclick', $.proxy(me._plotclick, me)); 837 | this.$graph.bind('plotselecting', $.proxy(me._plotselecting, me)); 838 | this.$graph.bind('plotselected', $.proxy(me._plotselected, me)); 839 | }; 840 | 841 | /** 842 | * hide and show zoom buttons 843 | * @param event 844 | * @private 845 | */ 846 | this._zoomButton = function (event) { 847 | var $clicked = $(event.currentTarget); 848 | 849 | if ($clicked.hasClass('zoom')) { 850 | // - turn on zooming 851 | // - hide the zoom button and show the cancel zoom button 852 | this.zooming = true; 853 | this.$container.find('.pauseZoom').show().parent().show(); 854 | this.$container.find('.cancelZoom').show().parent().show(); 855 | // show the selection range 856 | this.$container.find('.selectionContainer').show(); 857 | $clicked.hide().parent().hide(); 858 | } else if ($clicked.hasClass('pauseZoom')) { 859 | // pause the zooming 860 | this.zooming = false; 861 | this.$container.find('.resumeZoom').show().parent().show(); 862 | $clicked.hide().parent().hide(); 863 | } else if ($clicked.hasClass('resumeZoom')) { 864 | // resume zooming 865 | this.zooming = true; 866 | this.$container.find('.pauseZoom').show().parent().show(); 867 | $clicked.hide().parent().hide(); 868 | } else { 869 | // - turn off zooming 870 | // - show the zoom button 871 | // - reset the graph 872 | this.$container.find('.selectionContainer').hide(); 873 | this.$container.find('.zoom').show().parent().show(); 874 | this.zooming = false; 875 | this.plot = $.plot(this.$graph, this.series, this.chartOptions); 876 | this.setAverage(); 877 | this._fixUpLegend(); 878 | $clicked.hide().parent().hide(); 879 | this.$container.find('.pauseZoom').hide().parent().hide(); 880 | this.$container.find('.resumeZoom').hide().parent().hide(); 881 | } 882 | }; 883 | 884 | /** 885 | * show the tooltip when you hover over points on the graph 886 | * @param event 887 | * @param pos 888 | * @param item 889 | * @private 890 | */ 891 | this._plotHover = function (event, pos, item) { 892 | if (item) { 893 | if (this.previousHoverPoint != item.dataIndex) { 894 | 895 | this.previousHoverPoint = item.dataIndex; 896 | 897 | $("#tooltip").remove(); 898 | var x = item.datapoint[0].toFixed(2), 899 | date = this.convertDateToString(x), 900 | text = 'Click to see what you ate on this date: ' + date; 901 | 902 | if (this.zooming) { 903 | text = '* Pause / Reset zooming to click on a date! *'; 904 | } 905 | 906 | this._showTooltip(item.pageX, item.pageY, text); 907 | } 908 | } else { 909 | $("#tooltip").remove(); 910 | this.previousHoverPoint = null; 911 | } 912 | }; 913 | 914 | /** 915 | * open new page when point is clicked 916 | * @param event 917 | * @param pos 918 | * @param item 919 | * @private 920 | */ 921 | this._plotclick = function (event, pos, item) { 922 | if (!this.zooming) { 923 | if (item) { 924 | var x = item.datapoint[0].toFixed(2), 925 | date = this.convertDateToString(x, true); 926 | 927 | console.log('https://www.myfitnesspal.com/food/diary?date=' + date); 928 | window.open('https://www.myfitnesspal.com/food/diary?date=' + date, '_blank'); 929 | } 930 | } 931 | }; 932 | 933 | /** 934 | * when selecting a range to zoom in on, show the selected date range 935 | * @param event 936 | * @param ranges 937 | * @private 938 | */ 939 | this._plotselecting = function (event, ranges) { 940 | if (this.zooming && $.type(ranges) !== 'null') { 941 | var from = this.convertDateToString(ranges.xaxis.from.toFixed(1)), 942 | to = this.convertDateToString(ranges.xaxis.to.toFixed(1)), 943 | newData; 944 | 945 | this.$container.find('.selection').text(from + " to " + to); 946 | newData = this._getRangeOfData(ranges.xaxis.from, ranges.xaxis.to); 947 | this.setAverage(newData); 948 | } else { 949 | // not zooming, so clear the selection 950 | this.plot.clearSelection(); 951 | } 952 | }; 953 | 954 | /** 955 | * once an area on the graph has been selected, redraw the graph 956 | * @param event 957 | * @param ranges 958 | * @private 959 | */ 960 | this._plotselected = function (event, ranges) { 961 | var newData; 962 | 963 | if (this.zooming) { 964 | this.plot = $.plot(this.$graph, this.series, $.extend(true, {}, this.chartOptions, { 965 | xaxis: { 966 | min: ranges.xaxis.from, 967 | max: ranges.xaxis.to 968 | } 969 | })); 970 | 971 | // sign... isn't that cute, its returning a random *time* in a day, where 972 | // the user selected! Not an exact date whole date. So get the full 973 | // nearest date to where they were selecting. 974 | newData = this._getRangeOfData(ranges.xaxis.from, ranges.xaxis.to); 975 | this.setAverage(newData); 976 | this._fixUpLegend(); 977 | } else { 978 | // not zooming, so clear the selection 979 | this.plot.clearSelection(); 980 | } 981 | }; 982 | 983 | /** 984 | * take in range of and convert to dates, for range of data 985 | * @param axisFrom 986 | * @param axisTo 987 | * @returns {Array.|string|Blob|ArrayBuffer} 988 | * @private 989 | */ 990 | this._getRangeOfData = function (axisFrom, axisTo) { 991 | var from = this._parent.dates.indexOf(this._getFullDate(axisFrom)), 992 | to = this._parent.dates.indexOf(this._getFullDate(axisTo)), 993 | data = this._parent.allData[this.field].slice(from, to); 994 | 995 | return data; 996 | }; 997 | 998 | /** 999 | * get the current date from some random time and date; at midnight 1000 | * @param dateTime 1001 | * @returns {number} 1002 | * @private 1003 | */ 1004 | this._getFullDate = function (dateTime) { 1005 | var d = new Date(dateTime); 1006 | d.setHours(0,0,0,0); 1007 | return d.getTime(); 1008 | }; 1009 | 1010 | /** 1011 | * show the tooltip 1012 | * @param x 1013 | * @param y 1014 | * @param contents 1015 | * @private 1016 | */ 1017 | this._showTooltip = function (x, y, contents) { 1018 | $('
' + contents + '
').css({ 1019 | position: "absolute", 1020 | display: "none", 1021 | top: y + 5, 1022 | left: x + 5, 1023 | border: "1px solid #fdd", 1024 | padding: "2px", 1025 | "background-color": "#fee", 1026 | opacity: 0.80 1027 | }).appendTo("body").fadeIn(200); 1028 | }; 1029 | 1030 | } 1031 | SegmentGraph.prototype = new Graph(); 1032 | SegmentGraph.prototype.constructor = SegmentGraph; 1033 | 1034 | /** 1035 | * Provides an extended base graph to look back on previous timelines 1036 | * @extends {Graph} 1037 | * @constructor 1038 | */ 1039 | function LookbackGraph(){ 1040 | 1041 | /** 1042 | * Overrides the initialization configuration 1043 | */ 1044 | this.init = function(parent){ 1045 | return Object.getPrototypeOf(this).init.call(this, parent, '1', 'Look back on your progress', { 1046 | yaxes: [ 1047 | {show:false} 1048 | ] 1049 | }); 1050 | }; 1051 | 1052 | this.graphData = function(){ 1053 | 1054 | var progressSeries = [ 1055 | { data:[], weeksBefore:12, label:'3 months before', color:'#fefefe' }, 1056 | { data:[], weeksBefore:4, label:'A month before', color:'#fafafa' }, 1057 | { data:[], weeksBefore:3, label:'3 weeks before', color:'#ddd' }, 1058 | { data:[], weeksBefore:2, label:'2 weeks before', color:'#bbb' }, 1059 | { data:[], weeksBefore:1, label:'A week before', color:'#333' }, 1060 | { data:[], weeksBefore:0, label:'Current progress', color:'#090', lineWidth:3 }, 1061 | ], 1062 | progress = this._parent.allData['1'], 1063 | min = 10000, 1064 | mMin = Math.min, 1065 | i, x; 1066 | 1067 | for(i = 0; i < progress.length; i++) 1068 | min = mMin(progress[i][1], min); 1069 | 1070 | for(i = 0; i < progress.length; i++) 1071 | for(x = 0; x < progressSeries.length; x++) 1072 | progressSeries[x].data.push([ progress[i][0], ( progress[i - progressSeries[x].weeksBefore * 7 ] || [] )[1] || min ]); 1073 | 1074 | return Object.getPrototypeOf(this).graphData.call(this, progressSeries.map(function(item){ 1075 | return { 1076 | data: item.data, 1077 | curvedLines: { apply:true }, 1078 | lines: { show:true, lineWidth:item.lineWidth || 1 }, 1079 | label: item.label, 1080 | yaxis: 2, 1081 | color: item.color 1082 | }; 1083 | })); 1084 | }; 1085 | 1086 | /** 1087 | * show the tooltip when you hover over points on the graph 1088 | * @param event 1089 | * @param pos 1090 | * @param item 1091 | * @private 1092 | */ 1093 | this._plotHover = function (event, pos, item) { 1094 | if (item) { 1095 | if (this.previousHoverPoint != item.dataIndex) { 1096 | this.previousHoverPoint = item.dataIndex; 1097 | $("#tooltip").remove(); 1098 | var text = item.datapoint[1]; 1099 | if (this.zooming) 1100 | text = '* Pause / Reset zooming to click on a date! *'; 1101 | this._showTooltip(item.pageX, item.pageY, text); 1102 | } 1103 | } else { 1104 | $("#tooltip").remove(); 1105 | this.previousHoverPoint = null; 1106 | } 1107 | }; 1108 | 1109 | /** 1110 | * Simply override with an empty function. We don't need a click here. 1111 | * @private 1112 | */ 1113 | this._plotclick = function () {}; 1114 | 1115 | } 1116 | LookbackGraph.prototype = new SegmentGraph(); 1117 | LookbackGraph.prototype.constructor = LookbackGraph; 1118 | 1119 | 1120 | addLink('https://www.stevenirby.me/wp-content/uploads/2013/02/style.css'); 1121 | // figureout a better way to do the css, since github is setting a bad mine type 1122 | // addLink('http://raw.github.com/stevenirby/myfitnesspal-reports/master/css/style.css'); 1123 | addLink('https://code.jquery.com/ui/1.10.1/themes/base/jquery-ui.css'); 1124 | 1125 | // add jQuery 1126 | addScript('https://code.jquery.com/jquery-1.9.0.min.js', function () { 1127 | addScript('https://code.jquery.com/ui/1.10.1/jquery-ui.js', function () { 1128 | // add graphing library 1129 | $(document).ready(function () { 1130 | addScript('https://cdn.jsdelivr.net/gh/flot/flot@v0.8.3/jquery.flot.min.js', function () { 1131 | addScript('https://cdn.jsdelivr.net/gh/flot/flot@v0.8.3/jquery.flot.time.js', function () { 1132 | addScript('https://cdn.jsdelivr.net/gh/flot/flot@v0.8.2/jquery.flot.selection.js', function () { 1133 | window.Report = new Report().init(); 1134 | }); 1135 | }); 1136 | }); 1137 | }); 1138 | }); 1139 | }); 1140 | })(); 1141 | --------------------------------------------------------------------------------