diff options
-rw-r--r-- | media/archweb.css | 27 | ||||
-rw-r--r-- | media/visualize.js | 112 | ||||
-rw-r--r-- | settings.py | 1 | ||||
-rw-r--r-- | templates/public/index.html | 8 | ||||
-rw-r--r-- | templates/visualize/index.html | 43 | ||||
-rw-r--r-- | urls.py | 1 | ||||
-rw-r--r-- | visualize/__init__.py | 0 | ||||
-rw-r--r-- | visualize/models.py | 0 | ||||
-rw-r--r-- | visualize/tests.py | 0 | ||||
-rw-r--r-- | visualize/urls.py | 9 | ||||
-rw-r--r-- | visualize/views.py | 59 |
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 %} @@ -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: |