summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan McGee <dan@archlinux.org>2011-10-05 15:45:44 -0500
committerDan McGee <dan@archlinux.org>2011-10-05 15:45:44 -0500
commitd5063bd1d2cae79df7ce6e826c7413fed61ff9db (patch)
treec2e80d2cdcd1407ee5e65a2d3dd5f5d58a78d63b
parente157f942e96ba827aebf08dd253c866fec88beaa (diff)
Add package visualizations page
Why the hell not? Have fun clicking all the pretty buttons. Signed-off-by: Dan McGee <dan@archlinux.org>
-rw-r--r--media/archweb.css27
-rw-r--r--media/visualize.js112
-rw-r--r--settings.py1
-rw-r--r--templates/public/index.html8
-rw-r--r--templates/visualize/index.html43
-rw-r--r--urls.py1
-rw-r--r--visualize/__init__.py0
-rw-r--r--visualize/models.py0
-rw-r--r--visualize/tests.py0
-rw-r--r--visualize/urls.py9
-rw-r--r--visualize/views.py59
11 files changed, 256 insertions, 4 deletions
diff --git a/media/archweb.css b/media/archweb.css
index 0cadd7a7..eb0f0ca1 100644
--- a/media/archweb.css
+++ b/media/archweb.css
@@ -948,3 +948,30 @@ ul.admin-actions {
#archnavbar.anb-download ul li#anb-download a {
color: white !important;
}
+
+/* visualizations page */
+.visualize-buttons {
+ margin: 0.5em 0.33em;
+}
+
+ .visualize-buttons button.active {
+ depressed: true;
+ }
+
+.visualize-chart {
+ position: relative;
+ height: 500px;
+ margin: 0.33em;
+}
+
+#visualize-archrepo .treemap-cell {
+ border: solid 1px white;
+ overflow: hidden;
+ position: absolute;
+}
+
+ #visualize-archrepo .treemap-cell span {
+ padding: 3px;
+ font-size: 0.85em;
+ line-height: 1em;
+ }
diff --git a/media/visualize.js b/media/visualize.js
new file mode 100644
index 00000000..c1ea598b
--- /dev/null
+++ b/media/visualize.js
@@ -0,0 +1,112 @@
+function packages_treemap(chart_id, orderings, default_order) {
+ var jq_div = $(chart_id),
+ color = d3.scale.category20();
+ key_func = function(d) { return d.key; },
+ value_package_count = function(d) { return d.count; };
+
+ var treemap = d3.layout.treemap()
+ .size([jq_div.width(), jq_div.height()])
+ /*.sticky(true)*/
+ .value(value_package_count)
+ .sort(function(a, b) { return a.key < b.key; })
+ .children(function(d) { return d.data; });
+
+ var cell_html = function(d) {
+ if (d.children) {
+ return "";
+ }
+ return "<span>" + d.name + ": " + treemap.value()(d) + "</span>";
+ };
+
+ var d3_div = d3.select(jq_div.get(0));
+
+ var prop_px = function(prop, offset) {
+ return function(d) {
+ var dist = d[prop] + offset;
+ if (dist > 0) return dist + "px";
+ else return "0px";
+ };
+ };
+
+ var cell = function() {
+ /* the -1 offset comes from the border width we use in the CSS */
+ this.style("left", prop_px("x", 0)).style("top", prop_px("y", 0))
+ .style("width", prop_px("dx", -1)).style("height", prop_px("dy", -1));
+ };
+
+ var fetch_for_ordering = function(order) {
+ d3.json(order.url, function(json) {
+ var nodes = d3_div.data([json]).selectAll("div").data(treemap.nodes, key_func);
+ /* start out new nodes in the center of the picture area */
+ var w_center = jq_div.width() / 2;
+ var h_center = jq_div.height() / 2;
+ nodes.enter().append("div")
+ .attr("class", "treemap-cell")
+ .attr("title", function(d) { return d.name; })
+ .style("left", w_center + "px").style("top", h_center + "px")
+ .style("width", "0px").style("height", "0px")
+ .style("display", function(d) { return d.children ? "none" : null; })
+ .html(cell_html);
+ nodes.transition().duration(1500)
+ .style("background-color", function(d) { return d.children ? null : color(d[order.color_attr]); })
+ .call(cell);
+ nodes.exit().transition().duration(1500).remove();
+ });
+ };
+
+ /* start the callback for the default order */
+ fetch_for_ordering(orderings[default_order]);
+
+ var make_scale_button = function(name, valuefunc) {
+ var button_id = chart_id + "-" + name;
+ /* upon button click, attach new value function and redraw all boxes
+ * accordingly */
+ d3.select(button_id).on("click", function() {
+ d3_div.selectAll("div")
+ .data(treemap.value(valuefunc), key_func)
+ .html(cell_html)
+ .transition().duration(1500).call(cell);
+
+ /* drop off the '#' sign to convert id to a class prefix */
+ d3.selectAll("." + chart_id.substring(1) + "-scaleby")
+ .classed("active", false);
+ d3.select(button_id).classed("active", true);
+ });
+ };
+
+ /* each scale button tweaks our value, e.g. net size function */
+ make_scale_button("count", value_package_count);
+ make_scale_button("flagged", function(d) { return d.flagged; });
+ make_scale_button("csize", function(d) { return d.csize; });
+ make_scale_button("isize", function(d) { return d.isize; });
+
+ var make_group_button = function(name, order) {
+ var button_id = chart_id + "-" + name;
+ d3.select(button_id).on("click", function() {
+ fetch_for_ordering(order);
+
+ /* drop off the '#' sign to convert id to a class prefix */
+ d3.selectAll("." + chart_id.substring(1) + "-groupby")
+ .classed("active", false);
+ d3.select(button_id).classed("active", true);
+ });
+ };
+
+ $.each(orderings, function(k, v) {
+ make_group_button(k, v);
+ });
+
+ var resize_timeout = null;
+ var real_resize = function() {
+ resize_timeout = null;
+ d3_div.selectAll("div")
+ .data(treemap.size([jq_div.width(), jq_div.height()]), key_func)
+ .call(cell);
+ };
+ $(window).resize(function() {
+ if (resize_timeout) {
+ clearTimeout(resize_timeout);
+ }
+ resize_timeout = setTimeout(real_resize, 200);
+ });
+}
diff --git a/settings.py b/settings.py
index 18437098..51f9fcf6 100644
--- a/settings.py
+++ b/settings.py
@@ -109,6 +109,7 @@ INSTALLED_APPS = (
'public',
'south', # database migration support
'releng',
+ 'visualize',
)
PGP_SERVER = 'pgp.mit.edu:11371'
diff --git a/templates/public/index.html b/templates/public/index.html
index bea19e0f..b63876ac 100644
--- a/templates/public/index.html
+++ b/templates/public/index.html
@@ -26,10 +26,10 @@
<p>Our strong community is diverse and helpful, and we pride ourselves
on the range of skillsets and uses for Arch that stem from it. Please
- check out our <a href="https://bbs.archlinux.org" title="Arch Forums">forums</a>
+ check out our <a href="https://bbs.archlinux.org/" title="Arch Forums">forums</a>
and <a href="http://mailman.archlinux.org/mailman/listinfo/"
title="Arch Mailing Lists">mailing lists</a>
- to get your feet wet. Also glance through our <a href="https://wiki.archlinux.org"
+ to get your feet wet. Also glance through our <a href="https://wiki.archlinux.org/"
title="Arch Wiki">wiki</a>
if you want to learn more about Arch.</p>
@@ -174,8 +174,8 @@
title="View/search the package repository database">Packages</a></li>
<li><a href="/groups/"
title="View the available package groups">Package Groups</a></li>
- <li><a href="https://bugs.archlinux.org/"
- title="Report/track bugs or make feature requests">Bug Tracker</a></li>
+ <li><a href="{% url visualize-index %}"
+ title="View visualizations">Visualizations</a></li>
<li><a href="{% url page-svn %}"
title="View SVN entries for packages">SVN Repositories</a></li>
<li><a href="http://projects.archlinux.org/"
diff --git a/templates/visualize/index.html b/templates/visualize/index.html
new file mode 100644
index 00000000..99525e69
--- /dev/null
+++ b/templates/visualize/index.html
@@ -0,0 +1,43 @@
+{% extends "base.html" %}
+
+{% block title %}Arch Linux - Visualizations{% endblock %}
+
+{% block content %}
+<div class="box">
+
+ <h2>Visualizations of Packaging Data</h2>
+
+ <h3>Package Treemap</h3>
+
+ <div class="visualize-buttons">
+ <div>
+ <span>Scale Using:</span>
+ <button id="visualize-archrepo-count" class="visualize-archrepo-scaleby active">Package Count</button>
+ <button id="visualize-archrepo-flagged" class="visualize-archrepo-scaleby">Flagged</button>
+ <button id="visualize-archrepo-csize" class="visualize-archrepo-scaleby">Compressed Size</button>
+ <button id="visualize-archrepo-isize" class="visualize-archrepo-scaleby">Installed Size</button>
+ </div>
+ <div>
+ <span>Group By:</span>
+ <button id="visualize-archrepo-repo" class="visualize-archrepo-groupby active">Repository</button>
+ <button id="visualize-archrepo-arch" class="visualize-archrepo-groupby">Architecture</button>
+ </div>
+ </div>
+ <div id="visualize-archrepo" class="visualize-chart"></div>
+</div>
+
+{% load cdn %}{% jquery %}
+<script type="text/javascript" src="/media/d3.min.js"></script>
+<script type="text/javascript" src="/media/d3.layout.min.js"></script>
+<script type="text/javascript" src="/media/archweb.js"></script>
+<script type="text/javascript" src="/media/visualize.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ var orderings = {
+ "repo": { url: "{% url visualize-byrepo %}", color_attr: "repo" },
+ "arch": { url: "{% url visualize-byarch %}", color_attr: "arch" },
+ };
+ packages_treemap("#visualize-archrepo", orderings, "repo");
+});
+</script>
+{% endblock %}
diff --git a/urls.py b/urls.py
index c9faf165..cdae51bf 100644
--- a/urls.py
+++ b/urls.py
@@ -76,6 +76,7 @@ urlpatterns += patterns('',
(r'^packages/', include('packages.urls')),
(r'^releng/', include('releng.urls')),
(r'^todo/', include('todolists.urls')),
+ (r'^visualize/', include('visualize.urls')),
(r'^opensearch/packages/$', 'packages.views.opensearch',
{}, 'opensearch-packages'),
(r'^todolists/$','todolists.views.public_list'),
diff --git a/visualize/__init__.py b/visualize/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/visualize/__init__.py
diff --git a/visualize/models.py b/visualize/models.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/visualize/models.py
diff --git a/visualize/tests.py b/visualize/tests.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/visualize/tests.py
diff --git a/visualize/urls.py b/visualize/urls.py
new file mode 100644
index 00000000..57ee0626
--- /dev/null
+++ b/visualize/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import patterns
+
+urlpatterns = patterns('visualize.views',
+ (r'^$', 'index', {}, 'visualize-index'),
+ (r'^by_arch/$', 'by_arch', {}, 'visualize-byarch'),
+ (r'^by_repo/$', 'by_repo', {}, 'visualize-byrepo'),
+)
+
+# vim: set ts=4 sw=4 et:
diff --git a/visualize/views.py b/visualize/views.py
new file mode 100644
index 00000000..68f5d4a5
--- /dev/null
+++ b/visualize/views.py
@@ -0,0 +1,59 @@
+from django.db.models import Count, Sum
+from django.http import HttpResponse
+from django.utils import simplejson
+from django.views.decorators.cache import cache_page
+from django.views.generic.simple import direct_to_template
+
+from main.models import Package, Arch, Repo
+
+def index(request):
+ return direct_to_template(request, 'visualize/index.html', {})
+
+def arch_repo_data():
+ qs = Package.objects.select_related().values(
+ 'arch__name', 'repo__name').annotate(
+ count=Count('pk'), csize=Sum('compressed_size'),
+ isize=Sum('installed_size'),
+ flagged=Count('flag_date')).order_by()
+ arches = Arch.objects.values_list('name', flat=True)
+ repos = Repo.objects.values_list('name', flat=True)
+
+ # now transform these results into two mappings: one ordered (repo, arch),
+ # and one ordered (arch, repo).
+ arch_groups = dict((a, { 'name': a, 'key': ':%s' % a, 'arch': a, 'repo': None, 'data': [] }) for a in arches)
+ repo_groups = dict((r, { 'name': r, 'key': '%s:' % r, 'arch': None, 'repo': r, 'data': [] }) for r in repos)
+ for row in qs:
+ arch = row['arch__name']
+ repo = row['repo__name']
+ values = {
+ 'arch': arch,
+ 'repo': repo,
+ 'name': '%s (%s)' % (repo, arch),
+ 'key': '%s:%s' % (repo, arch),
+ 'csize': row['csize'],
+ 'isize': row['isize'],
+ 'count': row['count'],
+ 'flagged': row['flagged'],
+ }
+ arch_groups[arch]['data'].append(values)
+ repo_groups[repo]['data'].append(values)
+
+ data = {
+ 'by_arch': { 'name': 'Architectures', 'data': arch_groups.values() },
+ 'by_repo': { 'name': 'Repositories', 'data': repo_groups.values() },
+ }
+ return data
+
+@cache_page(1800)
+def by_arch(request):
+ data = arch_repo_data()
+ to_json = simplejson.dumps(data['by_arch'], ensure_ascii=False)
+ return HttpResponse(to_json, mimetype='application/json')
+
+@cache_page(1800)
+def by_repo(request):
+ data = arch_repo_data()
+ to_json = simplejson.dumps(data['by_repo'], ensure_ascii=False)
+ return HttpResponse(to_json, mimetype='application/json')
+
+# vim: set ts=4 sw=4 et: