From d5063bd1d2cae79df7ce6e826c7413fed61ff9db Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 5 Oct 2011 15:45:44 -0500 Subject: Add package visualizations page Why the hell not? Have fun clicking all the pretty buttons. Signed-off-by: Dan McGee --- media/archweb.css | 27 ++++++++++ media/visualize.js | 112 +++++++++++++++++++++++++++++++++++++++++ settings.py | 1 + templates/public/index.html | 8 +-- templates/visualize/index.html | 43 ++++++++++++++++ urls.py | 1 + visualize/__init__.py | 0 visualize/models.py | 0 visualize/tests.py | 0 visualize/urls.py | 9 ++++ visualize/views.py | 59 ++++++++++++++++++++++ 11 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 media/visualize.js create mode 100644 templates/visualize/index.html create mode 100644 visualize/__init__.py create mode 100644 visualize/models.py create mode 100644 visualize/tests.py create mode 100644 visualize/urls.py create mode 100644 visualize/views.py 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 "" + d.name + ": " + treemap.value()(d) + ""; + }; + + 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 @@

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 forums + check out our forums and mailing lists - to get your feet wet. Also glance through our wiki if you want to learn more about Arch.

@@ -174,8 +174,8 @@ title="View/search the package repository database">Packages
  • Package Groups
  • -
  • Bug Tracker
  • +
  • Visualizations
  • SVN Repositories
  • + +

    Visualizations of Packaging Data

    + +

    Package Treemap

    + +
    +
    + Scale Using: + + + + +
    +
    + Group By: + + +
    +
    +
    + + +{% load cdn %}{% jquery %} + + + + + +{% 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 diff --git a/visualize/models.py b/visualize/models.py new file mode 100644 index 00000000..e69de29b diff --git a/visualize/tests.py b/visualize/tests.py new file mode 100644 index 00000000..e69de29b 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: -- cgit v1.2.3-55-g3dc8