├── .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 | 
19 |
20 | Into this:
21 |
22 | 
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 | '
Generating Report Page
Please wait...
Downloading data for:
',
140 | ''
141 | ],
142 | markup = [
143 | '',
144 | '
Your Progress at a Glance
',
145 | '
',
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 | '
',
688 | '
',
689 | '
',
690 | '
',
691 | '
',
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 |
--------------------------------------------------------------------------------