- Timestamp:
- Mar 12, 2019, 12:47:38 PM (5 years ago)
- Branches:
- current
- Children:
- 8ba32d9
- Parents:
- d9453fc
- Location:
- ow
- Files:
-
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
ow/static/css/main.css
rd9453fc r02b96c5 1552 1552 -moz-outline-style: none; 1553 1553 } 1554 /* Calendar heatmap */ 1555 .calendar-heatmap { 1556 margin: 20px; 1557 } 1558 .calendar-heatmap .month { 1559 margin-right: 8px; 1560 } 1561 .calendar-heatmap .month-name { 1562 font-size: 85%; 1563 fill: #777; 1564 } 1565 .calendar-heatmap .day-name { 1566 font-size: 85%; 1567 fill: #777; 1568 } 1569 .calendar-heatmap .day:hover { 1570 stroke: #e1e1e1; 1571 stroke-width: 2; 1572 } 1573 .calendar-heatmap .day:focus { 1574 stroke: #e1e1e1; 1575 stroke-width: 2; 1576 } 1554 1577 .verify-account-content { 1555 1578 background-position: center; -
ow/static/js/ow.js
rd9453fc r02b96c5 459 459 }; 460 460 461 owjs.calendar_heatmap_chart = function(spec) { 462 463 "use strict" 464 465 var chart_selector = spec.chart_selector, 466 url = spec.url, 467 day_names_list = spec.day_names_list; 468 469 var calendar_rows = function(month) { 470 /* 471 Given a date object that "marks" the beginning of a month 472 return the number of "rows" we need to show all days in that 473 month 474 */ 475 // m will be actually the last day of the previous month 476 var m = d3.timeMonth.floor(month); 477 return d3.timeWeeks(d3.timeWeek.floor(m), 478 d3.timeMonth.offset(m,1)).length; 479 } 480 481 // defaults for the squares/"cells" we will show with the info 482 var cell_margin = 3, 483 cell_size = 25; 484 485 486 var render = function render() { 487 /* 488 Method to actually perform all the loading and rendering of 489 the chart 490 */ 491 var chart = d3.select(chart_selector); 492 493 d3.json(url, {credentials: "same-origin"}).then(function (data) { 494 495 var min_date = d3.min(data, function(d) { 496 return new Date(d.day); 497 }); 498 499 var max_date = d3.max(data, function(d) { 500 return new Date(d.day); 501 }); 502 503 // sunday-starting week: 504 // day = d3.timeFormat("%w") 505 // week = d3.timeFormat("%W") 506 // monday-starting week: 507 // day = function(d) {return d3.timeFormat("%u")(d) - 1} 508 // week = d3.timeFormat("%W") 509 var day = function(d) {return d3.timeFormat("%u")(d) - 1}, 510 week = d3.timeFormat("%W"), 511 format = d3.timeFormat("%Y-%m-%d"), 512 titleFormat = d3.utcFormat("%a, %d %b"), 513 monthName = d3.timeFormat("%B / %Y"), 514 months = d3.timeMonth.range(d3.timeMonth.floor(min_date), max_date), 515 rows = calendar_rows(max_date); 516 517 // Build the svg image where the chart will be 518 var svg = chart.selectAll("svg") 519 .data(months) 520 .enter().append("svg") 521 .attr("class", "month") 522 .attr("width", (cell_size * 7) + (cell_margin * 8)) 523 .attr("height", function(d) { 524 // we add 50 extra so the month/year label fits 525 return (cell_size * rows) + (cell_margin * (rows + 1)) + 50; 526 }) 527 .append("g"); 528 529 // This adds the month/year label above the chart 530 svg.append("text") 531 .attr("class", "month-name") 532 .attr("x", ((cell_size * 7) + (cell_margin * 8)) / 2 ) 533 .attr("y", 15) 534 .attr("text-anchor", "middle") 535 .text(function(d) { return monthName(d); }); 536 537 // Now, go through each day and add a square/cell for them 538 var rect = svg.selectAll("rect.day") 539 .data(function(d, i) { 540 return d3.timeDays( 541 d, new Date(d.getFullYear(), d.getMonth()+1, 1)); 542 }) 543 .enter().append("rect") 544 .attr("class", "day") 545 .attr("width", cell_size) 546 .attr("height", cell_size) 547 .attr("rx", 6).attr("ry", 6) // rounded corners 548 .attr("fill", '#eaeaea') // default light grey fill 549 .attr("x", function(d) { 550 return (day(d) * cell_size) + (day(d) * cell_margin) + cell_margin; 551 }) 552 .attr("y", function(d) { 553 var base_value = (week(d) - week(new Date(d.getFullYear(), d.getMonth(), 1))); 554 return (base_value * cell_size) + (base_value * cell_margin) + cell_margin + 20; 555 }) 556 .on("mouseover", function(d) { 557 d3.select(this).classed('hover', true); 558 }) 559 .on("mouseout", function(d) { 560 d3.select(this).classed('hover', false); 561 }) 562 .datum(format); 563 564 rect.append("title") 565 .text(function(d) {return titleFormat(new Date(d));}); 566 567 // Add the row with the names of the days 568 var day_names = svg.selectAll('.day-name') 569 .data(day_names_list) 570 .enter().append("text") 571 .attr("class", "day-name") 572 .attr("width", cell_size) 573 .attr("height", cell_size) 574 .attr("x", function(d) { 575 return (day_names_list.indexOf(d) * cell_size) + (day_names_list.indexOf(d) * cell_margin) + cell_margin * 2; 576 }) 577 .attr("y", function(d) { 578 return ((cell_size * rows) + (cell_margin * (rows + 1))) + 40; 579 }) 580 .text(function(d) { 581 return d; 582 }); 583 584 var find_day = function(day) { 585 var found = data.find(function(d) { 586 return d.day == day; 587 }); 588 return found; 589 } 590 591 var lookup = d3.nest() 592 .key(function(d) { 593 return d.day; 594 }) 595 .rollup(function(leaves) { 596 return leaves[0].time; 597 }) 598 .object(data); 599 600 var count = d3.nest() 601 .key(function(d) { 602 return d.day; 603 }) 604 .rollup(function(leaves) { 605 return leaves[0].time; 606 }) 607 .entries(data); 608 609 var scale = d3.scaleLinear() 610 .domain(d3.extent(count, function(d) { 611 return d.value; 612 })) 613 .range(['#f8b5be', '#f60002']); 614 615 rect.filter(function(d) { 616 return d in lookup; 617 }) 618 .style("fill", function(d) { 619 // Fill in with the proper color 620 return scale([lookup[d]]); 621 }) 622 .classed("clickable", true) 623 .on("click", function(d){ 624 if(d3.select(this).classed('focus')){ 625 d3.select(this).classed('focus', false); 626 } else { 627 d3.select(this).classed('focus', true) 628 } 629 // doSomething(); 630 }) 631 .select("title") 632 .text(function(d) { 633 // Update the title adding some more info 634 var day = find_day(d); 635 return titleFormat(new Date(d)) + ": " + day.time_formatted; }); 636 }); 637 638 }; 639 640 var that = {} 641 that.render = render; 642 return that 643 644 }; 645 461 646 462 647 owjs.map_shots = function(spec) { -
ow/static/less/pages/profile.less
rd9453fc r02b96c5 154 154 } 155 155 } 156 157 158 /* Calendar heatmap */ 159 .calendar-heatmap { 160 margin: 20px; 161 .month { 162 margin-right: 8px; 163 } 164 .month-name { 165 font-size: 85%; 166 fill: #777; 167 } 168 .day-name { 169 font-size: 85%; 170 fill: #777; 171 } 172 .day { 173 &:hover { 174 stroke: #e1e1e1; 175 stroke-width: 2; 176 } 177 &:focus { 178 stroke: #e1e1e1; 179 stroke-width: 2; 180 } 181 } 182 } -
ow/templates/profile.pt
rd9453fc r02b96c5 51 51 </ul> 52 52 </div> 53 <div class="calendar-heatmap js-calendar-heatmap"> 54 </div> 53 55 </div> 54 56 … … 462 464 sport_stats.setup(); 463 465 466 var heatmap_chart = owjs.calendar_heatmap_chart({ 467 chart_selector: 'div.js-calendar-heatmap', 468 url: "${request.resource_url(user, 'month')}", 469 // Trick to have all those shortened day names translated 470 day_names_list: "${_('Mo Tu We Th Fr Sa Su')}".split(' ') 471 }); 472 heatmap_chart.render(); 473 464 474 var year_stats = owjs.year_stats({ 465 475 link_selector: 'a.js-choose-year-stats', -
ow/views/user.py
rd9453fc r02b96c5 438 438 } 439 439 json_stats.append(day_stats) 440 return Response(content_type='application/json', 441 charset='utf-8', 442 body=json.dumps(json_stats)) 443 444 445 @view_config( 446 context=User, 447 permission='view', 448 name='month') 449 def month_stats(context, request): 450 """ 451 For the given month, return a json-encoded stream containing 452 per-day workouts information. 453 """ 454 localizer = get_localizer(request) 455 now = datetime.now(timezone.utc) 456 year = int(request.GET.get('year', now.year)) 457 month = int(request.GET.get('month', now.month)) 458 workouts = context.workouts(year, month) 459 stats = {} 460 461 for workout in workouts: 462 start = workout.start.strftime('%Y-%m-%d') 463 if start not in stats.keys(): 464 stats[start] = { 465 'time': 0, # seconds 466 'distance': 0, # kilometers 467 'elevation': 0, # meters 468 } 469 duration = getattr(workout, 'duration', None) or timedelta(0) 470 stats[start]['time'] += duration.seconds 471 distance = getattr(workout, 'distance', None) or 0 472 stats[start]['distance'] += int(round(distance)) 473 elevation = getattr(workout, 'uphill', None) or 0 474 stats[start]['elevation'] += int(elevation) 475 476 json_stats = [] 477 for day in stats.keys(): 478 hms = timedelta_to_hms(timedelta(seconds=stats[day]['time'])) 479 hours_label = _('hour') 480 if hms[0] > 1: 481 hours_label = _('hours') 482 time_formatted = ' '.join([ 483 str(hms[0]).zfill(2), localizer.translate(hours_label), 484 str(hms[1]).zfill(2), localizer.translate(_('min.')) 485 ]) 486 json_stats.append({ 487 'day': day, 488 'time': stats[day]['time'], 489 'time_formatted': time_formatted, 490 'distance': stats[day]['distance'], 491 'elevation': stats[day]['elevation'] 492 }) 493 440 494 return Response(content_type='application/json', 441 495 charset='utf-8',
Note: See TracChangeset
for help on using the changeset viewer.