From cf4964059f5daba2088d3f2475c2c0d81d68ebd8 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Mon, 14 Dec 2015 13:04:34 +0000 Subject: [PATCH] Fix some issues Henrik found, and implement some of his suggestions: * Fix ancestor stacking * Suppress x-axis labels that overlap with others * Make bars clickable links - Legacy-Id: 10590 --- ietf/doc/utils.py | 7 +- ietf/static/ietf/css/ietf.css | 1 + ietf/static/ietf/js/document_timeline.js | 94 +++++++++++++++++------- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index fbcf075f9..11ab291f6 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -8,6 +8,7 @@ from django.conf import settings from django.db.models.query import EmptyQuerySet from django.forms import ValidationError from django.utils.html import strip_tags, escape +from django.core.urlresolvers import reverse as urlreverse from ietf.doc.models import Document, DocHistory, State from ietf.doc.models import DocAlias, RelatedDocument, BallotType, DocReminder @@ -561,7 +562,8 @@ def crawl_history(doc): retval.append({ 'name': d.name, 'rev': e.newrevisiondocevent.rev, - 'published': e.time.isoformat() + 'published': e.time.isoformat(), + 'url': urlreverse("doc_view", kwargs=dict(name=d)) + e.newrevisiondocevent.rev + "/" }) if doc.type_id == "draft": @@ -572,6 +574,7 @@ def crawl_history(doc): retval.append({ 'name': e.doc.canonical_name(), 'rev': e.doc.canonical_name(), - 'published': e.time.isoformat() + 'published': e.time.isoformat(), + 'url': urlreverse("doc_view", kwargs=dict(name=e.doc)) }) return sorted(retval, key=lambda x: x['published']) diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index df38e0769..4a7732424 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -443,6 +443,7 @@ form.navbar-form input.form-control.input-sm { width: 141px; } #timeline .bar text { fill: white; dominant-baseline: central; + pointer-events: none; } /* like label-success */ diff --git a/ietf/static/ietf/js/document_timeline.js b/ietf/static/ietf/js/document_timeline.js index 47bfba7d0..86a1860d7 100644 --- a/ietf/static/ietf/js/document_timeline.js +++ b/ietf/static/ietf/js/document_timeline.js @@ -8,19 +8,36 @@ var y_label_width; var x_axis; var width; -function offset(d, i) { - // increase the y offset if the document name changed in this revision - if (i > 0 && data[i - 1].name !== d.name || d.rev.match("^rfc\d+$")) { - bar_y += bar_height; + +function expiration_date(d) { + return new Date(d.published.getTime() + 1000 * 60 * 60 * 24 * 185); +} + + +function max(arr) { + return Math.max.apply(null, Object.keys(arr).map(function(e) { + return arr[e]; + })); +} + + +function offset(d) { + if (bar_y[d.name] === undefined) { + var m = Object.keys(bar_y).length === 0 ? -bar_height : max(bar_y); + bar_y[d.name] = m + bar_height; } - return "translate(" + x_scale(d.published) + ", " + bar_y + ")"; + return "translate(" + x_scale(d.published) + ", " + bar_y[d.name] + ")"; } function bar_width(d, i) { - if (i < data.length - 1) { - return x_scale(data[i + 1].published) - x_scale(d.published); + // check for next rev of this name + for (i++; i < data.length; i++) { + if (data[i].name === d.name) { break; } } + + var w = i === data.length ? expiration_date(d) : data[i].published; + return x_scale(w) - x_scale(d.published); } @@ -33,10 +50,21 @@ function scale_x() { d3.max(data, function(d) { return d.published; }) ]).range([y_label_width, width]); + // resort data by publication time to suppress some ticks if they are closer + // than 12px, and don't add a tick for the final pseudo entry + var tv = data + .slice(0, -1) + .sort(function(a, b) { return a.published - b.published; }) + .map(function(d, i, arr) { + if (i === 0 || + x_scale(d.published) > x_scale(arr[i - 1].published) + 12) { + return d.published; + } + }).filter(function(d) { return d !== undefined; }); + x_axis = d3.svg.axis() .scale(x_scale) - // don't add a tick for the pseudo entry - .tickValues(data.slice(0, -1).map(function(d) { return d.published; })) + .tickValues(tv) .tickFormat(d3.time.format("%b %Y")) .orient("bottom"); } @@ -51,10 +79,11 @@ function update_x_axis() { function update_timeline() { - bar_y = 0; + bar_y = {}; scale_x(); var chart = d3.select("#timeline svg").attr("width", width); - var bar = chart.selectAll("g").data(data); + // enter data (skip the last pseudo entry) + var bar = chart.selectAll("g").data(data.slice(0, -1)); bar.attr("transform", offset).select("rect").attr("width", bar_width); update_x_axis(); } @@ -84,7 +113,7 @@ function draw_timeline() { }); var y_labels = data - .map(function(elem) { return elem.name; }) + .map(function(d) { return d.name; }) .filter(function(val, i, self) { return self.indexOf(val) === i; }); // calculate the width of the widest y axis label by drawing them off-screen @@ -107,19 +136,28 @@ function draw_timeline() { // update update_timeline(); - // enter - var bar = chart.selectAll("g").data(data); + // re-order data by document name, for CSS background color alternation + var ndata = []; + y_labels.forEach(function(l) { + ndata = ndata.concat(data.filter(function(d) { return d.name === l; })); + }); + data = ndata; + + // enter data (skip the last pseudo entry) + var bar = chart.selectAll("g").data(data.slice(0, -1)); var g = bar.enter() .append("g") .attr({ class: "bar", transform: offset }); - g.append("rect") - .attr({ - height: bar_height, - width: bar_width - }); + g.append("a") + .attr("xlink:href", function(d) { return d.url; }) + .append("rect") + .attr({ + height: bar_height, + width: bar_width + }); g.append("text") .attr({ x: 3, @@ -129,11 +167,11 @@ function draw_timeline() { // since the gradient is defined inside the SVG, we need to set the CSS // style here, so the relative URL works - $("#timeline .bar:nth-last-child(2) rect").css("fill", "url(#gradient)"); + $("#timeline .bar:last-child rect").css("fill", "url(#gradient)"); var y_scale = d3.scale.ordinal() .domain(y_labels) - .rangePoints([0, bar_y]); + .rangePoints([0, max(bar_y) + bar_height]); var y_axis = d3.svg.axis() .scale(y_scale) @@ -142,7 +180,7 @@ function draw_timeline() { chart.append("g").attr({ class: "x axis", - transform: "translate(0, " + bar_y + ")" + transform: "translate(0, " + (max(bar_y) + bar_height) + ")" }); update_x_axis(); @@ -160,7 +198,7 @@ function draw_timeline() { d3.select(".x.axis").each(function() { x_label_height = this.getBBox().height; }); - chart.attr("height", bar_y + x_label_height); + chart.attr("height", max(bar_y) + bar_height + x_label_height); } @@ -172,10 +210,12 @@ d3.json("doc.json", function(error, json) { // make js dates out of publication dates data.forEach(function(d) { d.published = new Date(d.published); }); - // add pseudo entry 185 days after last rev (when the ID will expire) - var pseudo = new Date(data[data.length - 1].published.getTime() + - 1000 * 60 * 60 * 24 * 185); - data.push({name: "", rev: "", published: pseudo}); + // add pseudo entry when the ID will expire + data.push({ + name: "", + rev: "", + published: expiration_date(data[data.length - 1]) + }); draw_timeline(); } });