Add submission activity chart
This commit is contained in:
parent
61587679f7
commit
528f229b79
5 changed files with 309 additions and 3 deletions
|
@ -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
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
@import "vars";
|
@import "vars";
|
||||||
|
|
||||||
$table_header_rounding: 6px;
|
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import "vars";
|
||||||
|
|
||||||
@media(min-width: 400px) {
|
@media(min-width: 400px) {
|
||||||
#content-right {
|
#content-right {
|
||||||
&.users {
|
&.users {
|
||||||
|
@ -294,3 +296,100 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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">«</a>
|
||||||
|
<span id="year"></span>
|
||||||
|
<a href="javascript:void(0)" id="next-year-action">»</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,6 +144,130 @@
|
||||||
{% 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">
|
||||||
|
|
Loading…
Reference in a new issue