As far as I can tell, django-tagging will only create a cloud for one particular database model. This is usually awesome, but sometimes not enough. What if, for instance, you wanted to make a combined cloud that consolidated tags across more than one model? Maybe your blog puts tags not just on posts, but also on tweets and bookmarks.
That's pretty much the situation I was in, and here's how I hacked on the module's codebase to get it done. The code below accepts a queryset of TaggedItem objects and returns a list of tags and font sizes you can use to format the cloud, as I did here.
"""
A modification of the tag cloud utilities in django-tagging.
Necessary so I can run a tagcloud for all the models, not just one.
Tip: It's good to submit a select_related query to avoid a ton of
database hits. This will run a join that really slims things down.
>>> cloud.calculate_cloud(TaggedItem.objects.select_related().all())
"""
import math
from django.utils.translation import ugettext as _
# Font size distribution algorithms
LOGARITHMIC, LINEAR = 1, 2
def _calculate_thresholds(min_weight, max_weight, steps):
delta = (max_weight - min_weight) / float(steps)
return [min_weight + i * delta for i in range(1, steps + 1)]
def _calculate_tag_weight(weight, max_weight, distribution):
"""
Logarithmic tag weight calculation is based on code from the
`Tag Cloud`_ plugin for Mephisto, by Sven Fuchs.
.. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud
"""
if distribution == LINEAR or max_weight == 1:
return weight
elif distribution == LOGARITHMIC:
return math.log(weight) * max_weight / math.log(max_weight)
raise ValueError(_('Invalid distribution algorithm specified: %s.') % distribution)
def _group_tagged_items(tagged_item_qs):
"""
Accepts a queryset of TaggedItem objects, groups them by tag, and then counts their frequency.
"""
tag_count = {}
for ti in tagged_item_qs:
try:
tag_count[ti.tag]['count'] += 1
except KeyError:
tag_count[ti.tag] = {'font-size': None, 'count': 1}
return tag_count
def calculate_cloud(tagged_items_qs, steps=4, distribution=LOGARITHMIC, min_count=5):
"""
Add a ``font_size`` attribute to each tag according to the
frequency of its use, as indicated by its ``count``
attribute.
``steps`` defines the range of font sizes - ``font_size`` will
be an integer between 1 and ``steps`` (inclusive).
``distribution`` defines the type of font size distribution
algorithm which will be used - logarithmic or linear. It must be
one of ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
"""
tag_counts = _group_tagged_items(tagged_items_qs)
if len(tag_counts) > 0:
counts = [i['count'] for i in tag_counts.values()]
min_weight = float(min(counts))
max_weight = float(max(counts))
thresholds = _calculate_thresholds(min_weight, max_weight, steps)
for tag in tag_counts.keys():
font_set = False
tag_weight = _calculate_tag_weight(tag_counts[tag]['count'], max_weight, distribution)
for i in range(steps):
if not font_set and tag_weight <= thresholds[i]:
tag_counts[tag]['font_size'] = i + 1
font_set = True
# Cut off the data at the minimum count
tag_list = [(k, v['font_size'], v['count']) for k,v in tag_counts.items() if v['count'] > min_count]
# Sort by count
tag_list.sort(lambda x,y:cmp(x[2], y[2]))
# Reverse that
tag_list.reverse()
return tag_list