Add submission activity chart

This commit is contained in:
cuom1999 2020-12-25 19:02:14 -06:00
parent 61587679f7
commit 528f229b79
5 changed files with 309 additions and 3 deletions

View file

@ -11,6 +11,8 @@ from django.contrib.auth.views import redirect_to_login
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import transaction from django.db import transaction
from django.db.models import Count, Max, Min from django.db.models import Count, Max, Min
from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractYear
from django.http import Http404, HttpResponseRedirect, JsonResponse from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
@ -149,6 +151,25 @@ class UserAboutPage(UserPage):
ratio = (max_ever - max_user) / (max_ever - min_ever) if max_ever != min_ever else 1.0 ratio = (max_ever - max_user) / (max_ever - min_ever) if max_ever != min_ever else 1.0
context['max_graph'] = max_user + ratio * delta context['max_graph'] = max_user + ratio * delta
context['min_graph'] = min_user + ratio * delta - delta context['min_graph'] = min_user + ratio * delta - delta
submissions = (
self.object.submission_set
.annotate(date_only=Cast('date', DateField()))
.values('date_only').annotate(cnt=Count('id'))
)
context['submission_data'] = mark_safe(json.dumps({
date_counts['date_only'].isoformat(): date_counts['cnt'] for date_counts in submissions
}))
context['submission_metadata'] = mark_safe(json.dumps({
'min_year': (
self.object.submission_set
.annotate(year_only=ExtractYear('date'))
.aggregate(min_year=Min('year_only'))['min_year']
),
}))
return context return context
# follow/unfollow user # follow/unfollow user

View file

@ -1,7 +1,5 @@
@import "vars"; @import "vars";
$table_header_rounding: 6px;
.table { .table {
border-spacing: 0; border-spacing: 0;
width: 100%; width: 100%;

View file

@ -1,3 +1,5 @@
@import "vars";
@media(min-width: 400px) { @media(min-width: 400px) {
#content-right { #content-right {
&.users { &.users {
@ -293,4 +295,101 @@ a.edit-profile {
} }
.unfollow:hover { .unfollow:hover {
background: darkred; background: darkred;
}
#submission-activity {
#submission-activity-actions {
text-align: center;
#prev-year-action, #next-year-action {
font-size: 1.75em;
}
#year {
font-size: 1.25em;
color: #444;
}
}
#submission-activity-display {
border: 1px solid $border_gray;
border-radius: $table_header_rounding;
.info-bar {
display: flex;
justify-content: space-between;
.info-table {
width: 15%;
min-width: 130px;
.info-table-text {
width: 8%;
}
}
}
.info-text {
font-size: 0.75em;
line-height: 1;
font-weight: 100;
color: #444;
}
#submission-total-count {
align-self: center;
padding-left: 8%;
font-size: 0.85em;
}
@media(max-width: 1000px) {
#submission-total-count {
padding-left: 5px;
}
}
table {
width: 100%;
padding: 5px;
th.submission-date-col {
width: 8%;
}
@media (max-width: 1000px) {
th.submission-date-col {
display: none;
}
}
td {
border-radius: 20%;
div {
margin-top: 100%;
}
&.activity-label {
position: relative;
white-space: nowrap;
}
&.activity-blank {
background-color: white;
}
&.activity-0 {
background-color: #ddd;
}
&.activity-1 {
background-color: #9be9a8;
}
&.activity-2 {
background-color: #40c463;
}
&.activity-3 {
background-color: #2f9c4c;
}
&.activity-4 {
background-color: #216e39;
}
}
}
}
} }

View file

@ -7,5 +7,6 @@ $announcement_red: #ae0000;
$base_font_size: 14px; $base_font_size: 14px;
$widget_border_radius: 4px; $widget_border_radius: 4px;
$table_header_rounding: 6px;
$monospace-fonts: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; $monospace-fonts: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;

View file

@ -59,6 +59,69 @@
<br> <br>
{% endif %} {% endif %}
<h4 id="submission-activity-header"></h4>
<div id="submission-activity" style="display: none;">
<div id="submission-activity-actions">
<a href="javascript:void(0)" id="prev-year-action">&laquo;</a>
&nbsp;<span id="year"></span>&nbsp;
<a href="javascript:void(0)" id="next-year-action">&raquo;</a>
</div>
<div id="submission-activity-display">
<table id="submission-activity-table">
<tr id="submission-0">
<th class="submission-date-col info-text">
{{ _('Mon') }}
</th>
</tr>
<tr id="submission-1">
<th class="submission-date-col info-text">
{{ _('Tues') }}
</th>
</tr>
<tr id="submission-2">
<th class="submission-date-col info-text">
{{ _('Wed') }}
</th>
</tr>
<tr id="submission-3">
<th class="submission-date-col info-text">
{{ _('Thurs') }}
</th>
</tr>
<tr id="submission-4">
<th class="submission-date-col info-text">
{{ _('Fri') }}
</th>
</tr>
<tr id="submission-5">
<th class="submission-date-col info-text">
{{ _('Sat') }}
</th>
</tr>
<tr id="submission-6">
<th class="submission-date-col info-text">
{{ _('Sun') }}
</th>
</tr>
</table>
<div class="info-bar">
<span id="submission-total-count" class="info-text">
</span>
<table class="info-table">
<tr>
<th class="info-table-text info-text">{{ _('Less') }}</th>
<td class="activity-0"><div></div></td>
<td class="activity-1"><div></div></td>
<td class="activity-2"><div></div></td>
<td class="activity-3"><div></div></td>
<td class="activity-4"><div></div></td>
<th class="info-table-text info-text">{{ _('More') }}</th>
<tr>
</table>
</div>
</div>
</div>
{% if rating %} {% if rating %}
<h4>Rating History</h4> <h4>Rating History</h4>
<div id="rating-chart"> <div id="rating-chart">
@ -81,10 +144,134 @@
{% include "mathjax-load.html" %} {% include "mathjax-load.html" %}
{% endif %} {% endif %}
<script type="text/javascript">
var submission_activity = {{ submission_data }};
var metadata = {{ submission_metadata }};
const activity_levels = 5; // 5 levels of activity
$(function () {
var active_tooltip = null;
function display_tooltip(where) {
if (active_tooltip !== null) {
active_tooltip.removeClass(['tooltipped', 'tooltipped-e', 'tooltipped-w']).removeAttr('aria-label');
}
if (where !== null) {
var day_num = parseInt(where.attr('data-day'));
var tooltip_direction = day_num < 183 ? 'tooltipped-e' : 'tooltipped-w';
where.addClass(['tooltipped', tooltip_direction])
.attr('aria-label', where.attr('data-submission-activity'));
}
active_tooltip = where;
}
function install_tooltips () {
display_tooltip(null);
$('.activity-label').each(function () {
var link = $(this);
link.hover(
function(e) {
display_tooltip(link);
},
function(e) {
display_tooltip(null);
}
);
});
}
var current_year = new Date().getFullYear();
var $div = $('#submission-activity');
function draw_contribution(year) {
$div.find('#submission-activity-table td').remove();
$div.find('#year').attr('data-year', year);
$div.find('#prev-year-action').css('display', year > metadata.min_year ? '' : 'none');
$div.find('#next-year-action').css('display', year < current_year ? '' : 'none');
var current_weekday = 0;
var start_day = new Date(year, 0, 1)
var end_day = new Date(year + 1, 0, 0);
if (year == current_year) {
end_day = new Date();
start_day = new Date(end_day.getFullYear() - 1, end_day.getMonth(), end_day.getDate() + 1);
$div.find('#year').text("{{ _('past year') }}");
} else {
$div.find('#year').text(year);
}
var days = [];
for (var day = start_day, day_num = 1; day <= end_day; day.setDate(day.getDate() + 1), day_num++) {
var isodate = day.toISOString().split('T')[0];
days.push({
date: new Date(day),
weekday: day.getDay(),
day_num: day_num,
activity: submission_activity[isodate] || 0,
});
}
var sum_activity = days.map(obj => obj.activity).reduce((a, b) => a + b, 0);
$div.find('#submission-total-count').text(
ngettext("%(cnt)d total submission", "%(cnt)d total submissions", sum_activity)
.replace("%(cnt)d", sum_activity)
)
if (year == current_year) {
$('#submission-activity-header').text(
ngettext("%(cnt)d submission in the last year", "%(cnt)d submissions in the last year", sum_activity)
.replace("%(cnt)d", sum_activity)
)
}
var max_activity = Math.max.apply(null, days.map(obj => obj.activity));
var diff = max_activity / (activity_levels - 1);
var activity_breakdown = [...Array(activity_levels).keys()].map(x => diff * x);
for (; current_weekday < days[0].weekday; current_weekday++) {
$div.find('#submission-' + current_weekday)
.append($('<td>').addClass('activity-blank').append('<div>'));
}
days.forEach(obj => {
var level = activity_breakdown.findIndex(x => x >= obj.activity);
var text =
ngettext("%(cnt)d submission on %(date)s", "%(cnt)d submissions on %(date)s", obj.activity)
.replace('%(cnt)d', obj.activity)
.replace(
'%(date)s',
obj.date.toLocaleDateString(
"{{ LANGUAGE_CODE }}",
{month: 'short', year: 'numeric', day: 'numeric'}
)
)
$div.find('#submission-' + obj.weekday)
.append(
$('<td>').addClass(['activity-label', 'activity-' + level])
.attr('data-submission-activity', text)
.attr('data-day', obj.day_num)
.append('<div>')
);
});
install_tooltips();
}
$('#prev-year-action').click(function () {
draw_contribution(parseInt($div.find('#year').attr('data-year')) - 1);
});
$('#next-year-action').click(function () {
draw_contribution(parseInt($div.find('#year').attr('data-year')) + 1);
});
draw_contribution(current_year);
$('#submission-activity').css('display', '');
});
</script>
{% if ratings %} {% if ratings %}
<script type="text/javascript" src="{{ static('libs/chart.js/Chart.js') }}"></script> <script type="text/javascript" src="{{ static('libs/chart.js/Chart.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
var rating_history = {{rating_data}}; var rating_history = {{ rating_data }};
$.each(rating_history, function (index, item) { $.each(rating_history, function (index, item) {
item.x = new Date(item.timestamp); item.x = new Date(item.timestamp);