Refactor graphical timeline, by making the data available via doc.json. Also

refactor associated js/css/html. Finally, a lot of display tweaks.
 - Legacy-Id: 10550
This commit is contained in:
Lars Eggert 2015-12-08 14:29:35 +00:00
parent e1ff3a5782
commit d4d09a2f75
5 changed files with 181 additions and 141 deletions

View file

@ -3,7 +3,6 @@ import re
import urllib
import math
import datetime
import operator
from django.conf import settings
from django.db.models.query import EmptyQuerySet
@ -542,7 +541,7 @@ def uppercase_std_abbreviated_name(name):
return name
def crawl_history(doc):
# return document history data for use in ietf/templates/doc/timeline.html
# return document history data for inclusion in doc.json (used by timeline)
def ancestors(doc):
retval = []
if hasattr(doc, 'relateddocument_set'):
@ -559,12 +558,20 @@ def crawl_history(doc):
for d in history:
for e in d.docevent_set.filter(type='new_revision'):
if hasattr(e, 'newrevisiondocevent'):
retval.append((d.name, e.newrevisiondocevent.rev, e.time.isoformat()))
retval.append({
'name': d.name,
'rev': e.newrevisiondocevent.rev,
'published': e.time.isoformat()
})
if doc.type_id == "draft":
e = doc.latest_event(type='published_rfc')
else:
e = doc.latest_event(type='iesg_approved')
if e:
retval.append((doc.name, e.doc.canonical_name, e.time.isoformat()))
return sorted(retval, key=operator.itemgetter(2))
retval.append({
'name': e.doc.canonical_name(),
'rev': e.doc.canonical_name(),
'published': e.time.isoformat()
})
return sorted(retval, key=lambda x: x['published'])

View file

@ -417,7 +417,6 @@ def document_main(request, name, rev=None):
search_archive=search_archive,
actions=actions,
tracking_document=tracking_document,
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
),
context_instance=RequestContext(request))
@ -443,8 +442,6 @@ def document_main(request, name, rev=None):
can_manage = can_manage_group_type(request.user, doc.group.type_id)
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
return render_to_response("doc/document_charter.html",
dict(doc=doc,
top=top,
@ -459,7 +456,6 @@ def document_main(request, name, rev=None):
group=group,
milestones=milestones,
can_manage=can_manage,
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
),
context_instance=RequestContext(request))
@ -477,8 +473,6 @@ def document_main(request, name, rev=None):
if doc.get_state_slug() in ("iesgeval") and doc.active_ballot():
ballot_summary = needed_ballot_positions(doc, doc.active_ballot().active_ad_positions().values())
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
return render_to_response("doc/document_conflict_review.html",
dict(doc=doc,
top=top,
@ -490,7 +484,6 @@ def document_main(request, name, rev=None):
conflictdoc=conflictdoc,
ballot_summary=ballot_summary,
approved_states=('appr-reqnopub-pend','appr-reqnopub-sent','appr-noprob-pend','appr-noprob-sent'),
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
),
context_instance=RequestContext(request))
@ -515,8 +508,6 @@ def document_main(request, name, rev=None):
else:
sorted_relations=None
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
return render_to_response("doc/document_status_change.html",
dict(doc=doc,
top=top,
@ -528,7 +519,6 @@ def document_main(request, name, rev=None):
ballot_summary=ballot_summary,
approved_states=('appr-pend','appr-sent'),
sorted_relations=sorted_relations,
rev_history=crawl_history(latest_revision.doc if latest_revision else doc),
),
context_instance=RequestContext(request))
@ -921,6 +911,9 @@ def document_json(request, name):
data["shepherd"] = doc.shepherd.formatted_email() if doc.shepherd else None
data["ad"] = doc.ad.role_email("ad").formatted_email() if doc.ad else None
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
data["rev_history"] = crawl_history(latest_revision.doc if latest_revision else doc)
if doc.type_id == "draft":
data["iesg_state"] = extract_name(doc.get_state("draft-iesg"))
data["rfceditor_state"] = extract_name(doc.get_state("draft-rfceditor"))
@ -932,7 +925,7 @@ def document_json(request, name):
data["consensus"] = e.consensus if e else None
data["stream"] = extract_name(doc.stream)
return HttpResponse(json.dumps(data, indent=2), content_type='text/plain')
return HttpResponse(json.dumps(data, indent=2), content_type='application/json')
class AddCommentForm(forms.Form):
comment = forms.CharField(required=True, widget=forms.Textarea)

View file

@ -0,0 +1,126 @@
var data;
d3.json("doc.json", function(error, json) {
if (error) return console.warn(error);
data = json["rev_history"];
// make js dates out of publication dates
data.forEach(function(el) { el.published = new Date(el.published); });
// add pseudo entry for beginning of year of first publication
var year = data[0].published.getFullYear();
data.unshift({ name:'', rev: '', published: new Date(year, 0, 0)});
// add pseudo entry at end of year of last revision
year = data[data.length - 1].published.getFullYear();
data.push({ name:'', rev: '', published: new Date(year + 1, 0, 0)});
draw_timeline();
});
var xscale;
var y;
var bar_height;
function offset(d, i) {
if (i > 1 && data[i - 1].name !== d.name || d.rev.match("rfc"))
y += bar_height;
return "translate(" + xscale(d.published) + ", " + y + ")";
}
function bar_width(d, i) {
if (i > 0 && i < data.length - 1)
return xscale(data[i + 1].published) - xscale(d.published);
}
function draw_timeline() {
var w = $("#timeline").width();
// bar_height = parseFloat($("body").css('line-height'));
bar_height = 30;
xscale = d3.time.scale().domain([
d3.min(data, function(d) { return d.published; }),
d3.max(data, function(d) { return d.published; })
]).range([0, w]);
y = 0;
var chart = d3.select("#timeline svg").attr("width", w);
var bar = chart.selectAll("g").data(data);
// update
bar
.attr("transform", offset)
.select("rect")
.attr("width", bar_width);
// enter
var g = bar.enter()
.append("g")
.attr({
class: "bar",
transform: offset
});
g.append("rect")
.attr({
height: bar_height,
width: bar_width
});
g.append("text")
.attr({
x: 3,
y: bar_height/2
})
.text(function(d) { return d.rev; });
// exit
bar.exit().remove();
var xaxis = d3.svg.axis()
.scale(xscale)
.tickValues(data.slice(1, -1).map(function(d) { return d.published; }))
.tickFormat(d3.time.format("%b %Y"))
.orient("bottom");
var ids = data
.map(function(elem) { return elem.name; })
.filter(function(val, i, self) { return self.indexOf(val) === i; });
ids.shift(); // first one is pseudo entry (last one, too, but filtered above)
console.log(ids);
var yaxis = d3.svg.axis()
.scale(d3.scale.ordinal().domain(ids).rangePoints([0, y - bar_height]))
.tickValues(ids)
.orient("left");
chart.append("g")
.attr({
class: "x axis",
transform: "translate(0, " + y + ")"
})
.call(xaxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "translate(-18, 8) rotate(-90)");
chart.append("g")
.attr({
class: "y axis",
transform: "translate(10, " + bar_height/2 + ")"
})
.call(yaxis)
.selectAll("text")
.style("text-anchor", "start");
chart.attr('height', y);
}
$(window).on({
resize: function (event) {
draw_timeline();
}
});

View file

@ -11,21 +11,39 @@
{% block morecss %}
.inline { display: inline; }
{% endblock %}
{% block js %}
<script>
$(window).on({
resize: function (event) {
draw_timeline();
},
load: function (event) {
draw_timeline();
#timeline .axis text { font-size: small; }
#timeline .axis path, #timeline .axis line {
fill: none;
stroke: black;
}
});
</script>
{% endblock %}
#timeline .axis.y path, #timeline .axis.y line { stroke: none; }
#timeline .axis.x text {
dominant-baseline: central;
fill: darkgrey;
}
#timeline .bar text {
fill: white;
dominant-baseline: central;
}
#timeline .bar:nth-child(odd) rect {
fill: #3abf03;
stroke: #32a602;
stroke-width: 1;
}
#timeline .bar:nth-child(even) rect {
fill: #6b5bad;
stroke: #5f4f9f;
stroke-width: 1;
}
{% endblock %}
{% block title %}
{% if doc.get_state_slug == "rfc" %}
@ -40,7 +58,7 @@ $(window).on({
{{ top|safe }}
{% include "doc/revisions_list.html" %}
{% include "doc/timeline.html" %}
<div id="timeline"><svg></svg></div>
<table class="table table-condensed">
<thead id="message-row">
@ -509,3 +527,9 @@ $(window).on({
{% endif %}
{% endblock %}
{% block js %}
<script src="{% static 'd3/d3.min.js' %}"></script>
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
{% endblock %}

View file

@ -1,110 +0,0 @@
{% load staticfiles %}
<script src="{% static 'd3/d3.min.js' %}"></script>
<style>
#timeline { width: 100%; }
#timeline text { fill: white; }
#timeline > :nth-child(odd) { fill: steelblue; }
#timeline > :nth-child(even) { fill: red; }
.axis path,
.axis line {
fill: none;
stroke: black;
}
#timeline .axis text {
fill: black;
font-size: small;
}
</style>
<svg id="timeline"></svg>
<script>
var data = [
{% for r in rev_history %}
{% if forloop.first %}
{ name: '{{ r.2 }}'.substring(0, 4), rev: '', time: new Date('{{ r.2 }}'.substring(0, 4))},
{% endif %}
{ name: '{{r.0}}', rev: '{{r.1}}', time: new Date('{{ r.2 }}')},
{% if forloop.last %}
{ name: (parseInt('{{ r.2 }}'.substring(0, 4)) + 1).toString(), rev: '', time: new Date((parseInt('{{ r.2 }}'.substring(0, 4)) + 1).toString())},
{% endif %}
{% endfor %}
];
var x;
var y;
var bar_height;
function offset(d, i) {
if (i > 1 && data[i - 1].name !== d.name || d.rev.match("rfc"))
y += bar_height;
return "translate(" + x(d.time) + ", " + y + ")";
}
function bar_width(d, i) {
if (i > 0 && i < data.length - 1)
return x(data[i + 1].time) - x(d.time);
}
function draw_timeline() {
var w = $("#timeline").width();
bar_height = parseFloat($("body").css('line-height'));
x = d3.time.scale().domain([
d3.min(data, function(d) { return d.time; }),
d3.max(data, function(d) { return d.time; })
]).range([0, w]);
y = 0;
var chart = d3.select("#timeline");
var bar = chart.selectAll("g").data(data);
// update
bar
.attr("transform", offset)
.select("rect")
.attr("width", bar_width);
// enter
var g = bar.enter()
.append("g")
.attr("transform", offset);
g.append("rect")
.attr({
height: bar_height,
width: bar_width
});
g.append("text")
.attr("y", bar_height/2)
.text(function (d) { return d.rev; });
// exit
bar.exit().remove();
chart.attr("height", y + 3*bar_height);
var axis = d3.svg.axis()
.scale(x)
.tickValues(data.slice(1, -1).map(function(d) { return d.time; }))
.tickFormat(d3.time.format("%b %Y"))
.orient("bottom");
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + y + ")")
.call(axis)
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "translate(-13, 10) rotate(-90)");
}
</script>