Weighted tag cloud

I’m trying to build a generic weighted tag cloud as in the final example at the bottom of https://24ways.org/2006/marking-up-a-tag-cloud

So far I have this CSS:

.tag-cloud li{display:inline}
.tag-cloud li span{position: absolute; left: -9999px; width: 90px}
.tag-cloud li.rare{font-size:1.0em}
.tag-cloud li.seldom{font-size:1.2em}
.tag-cloud li.occasional{font-size:1.4em}
.tag-cloud li.often{font-size:1.6em}
.tag-cloud li.frequent{font-size:1.8em}
.tag-cloud li.common{font-size:2.0em}

and this in the template:

{{ if not (eq (len .Site.Taxonomies.tags) 0) }}
{{ $.Scratch.SetInMap "weights" "0" "rare" }}
{{ $.Scratch.SetInMap "weights" "1" "seldom" }}
{{ $.Scratch.SetInMap "weights" "2" "occasional" }}
{{ $.Scratch.SetInMap "weights" "3" "often" }}
{{ $.Scratch.SetInMap "weights" "4" "frequent" }}
{{ $.Scratch.SetInMap "weights" "5" "common" }}
{{ $.Scratch.Set "total" 0 }}
{{ range $name, $items := .Site.Taxonomies.tags }}
  {{ $.Scratch.Add "total" (len $items) }}
{{ end }}
<pre>
total = {{ $.Scratch.Get "total" }}
weights = {{ $.Scratch.GetSortedMapValues "weights" }}
third = {{ index ($.Scratch.GetSortedMapValues "weights") 2 }}
</pre>
<ol class="tag-cloud">
{{ range $name, $items := .Site.Taxonomies.tags }}
  {{ if gt (len $items) 1 }}
    {{ $.Scratch.Set "count" (print (len $items) " " (pluralize "article")) }}
  {{ else }}
    {{ $.Scratch.Set "count" "1 article" }}
  {{ end }}
  <li class="">
    <a href="{{ $.Site.BaseURL }}tags/{{ $name | urlize | lower  }}" title="{{ $name }}: {{ $.Scratch.Get "count" }}">{{ $name }}<span> ({{ $.Scratch.Get "count" }})</span></a>
  </li>
{{ end }}
</ol>

In the <pre> block it correctly prints what I expect but I’m having trouble working out how to map the count to a weight. I had a look at Drupal’s implementation but it uses min(), max(), log() and floor() functions which aren’t currently available in Hugo’s template functions.

So before I go making a pull request for something specific that I need to make this work, I thought I would ask

  1. Is there a better way of building a tag list with weights?
  2. Should I implement some function to take .Site.Taxonomies.tags (which returns a map of name to an array of items) and a variable number of steps to return mapping of name to weight or is there something more generic I can build?

I’m thinking this may just be easier to do with jquery which is probably fine since it’s only really meaningful for visual browsers.

How is this going?

I know this is an old topic, but I ended up here myself by searching. I see that waddles is trying to avoid inline styles, but if you don’t mind that, you might find this useful. At least it works for me:

{{ if not (eq (len $.Site.Taxonomies.tags) 0) }}
    {{ $fontUnit := "rem" }}
    {{ $largestFontSize := 2.0 }}
    {{ $smallestFontSize := 1.0 }}
    {{ $fontSpread := sub $largestFontSize $smallestFontSize }}
    <!--<div>Font size unit: {{ $fontUnit }}</div>
    <div>Font min size: {{ $smallestFontSize }}</div>
    <div>Font max size: {{ $largestFontSize }}</div>
    <div>Font size spread: {{ $fontSpread }}</div>-->

    {{ $max := len (index $.Site.Taxonomies.tags.ByCount 0).Pages }}
    <!--<div>Max tag count: {{ $max }}</div>-->

    {{ $min := len (index $.Site.Taxonomies.tags.ByCount.Reverse 0).Pages }}
    <!--<div>Min tag count: {{ $min }}</div>-->

    {{ $spread := sub $max $min }}
    <!--<div>Tag count spread: {{ $spread }}</div>-->

    {{ $fontStep := div $fontSpread $spread }}    
    <!--<div>Font step: {{ $fontStep }}</div>-->

    <ol id="tag-cloud">
        {{ range $name, $taxonomy := $.Site.Taxonomies.tags }} 
            {{ $currentTagCount := len $taxonomy.Pages }}
            {{ $currentFontSize := (add $smallestFontSize (mul (sub $currentTagCount $min) $fontStep) ) }}
            <!--Current font size: {{$currentFontSize}}-->
            <li><a href="{{ "/tags/" | relLangURL }}{{ $name | urlize }}" style="font-size:{{$currentFontSize}}{{$fontUnit}}">{{ $name }} ({{$currentTagCount}})</a></li>
        {{ end }}
    </ol>
{{ end }}

Described in a bit more detail at: http://henrik.sommerfeld.nu/hugo-tag-could/

3 Likes

Cool. Other solutions I’ve seen are JS based.

Slightly safer version of @henriksommerfeld’s solution:

/partials/term_cloud.html

{{- if gt (len .Data.Terms) 0 -}}
  {{- $maxSize := 2.0 -}}
  {{- $minSize := 1.0 -}}
  {{- $sizeSpread := ( sub $maxSize $minSize ) -}}

  {{- $maxCount := (index .Data.Terms.ByCount 0).Count -}}
  {{- $minCount := (index .Data.Terms.ByCount.Reverse 0).Count -}}
  {{- $countSpread := ( sub $maxCount $minCount ) -}}

  {{- $.Scratch.Set "sizeStep" 0 -}}
  {{- if gt $countSpread 0 -}}
    {{- $.Scratch.Set "sizeStep" ( div $sizeSpread $countSpread ) -}}
  {{- end -}}

  <ul class='term-cloud'>
  {{- range .Data.Terms.Alphabetical -}}
    {{- $count := .Count -}}
    {{- $sizeStep := ( $.Scratch.Get "sizeStep" ) -}}
    {{- $size := ( add $minSize ( mul $sizeStep ( sub $count $minCount ) ) ) -}}
    <li>
      <a href='{{ $.Data.Plural | relURL }}/{{ .Term | urlize }}' style='font-size:{{ $size }}em'>
        {{- .Term -}}
      </a>
    </li>
  {{- end -}}
  </ul>
{{- end -}}

This takes care of some corner cases that can cause Hugo to crash.

This partial is written to be used on layouts/_defaults/terms.html template.

Javascript Shuffle

const shuffle = array => {
  let shuffled = [...array],
    currIndex = array.length,
    tempValue,
    randIndex

  while (currIndex) {
    randIndex = Math.floor(Math.random() * currIndex)
    currIndex--

    tempValue = shuffled[currIndex]
    shuffled[currIndex] = shuffled[randIndex]
    shuffled[randIndex] = tempValue
  }

  return shuffled
}

let termCloud = document.querySelector('.term-cloud')
if (termCloud) {
  let terms = termCloud.querySelectorAll('.term-cloud li')
  shuffle(terms).forEach(term => term.parentElement.appendChild(term))
}

Thanks for improving my version. I just tried my code on my own content and I’m a newbie at Hugo.

Would you mind explaining what the “corner cases that can cause Hugo to crash” are? I see the possible division by 0 when all posts have the same number of tags. Anything else?

For me your code crashes at line 1 with <len .Data.Terms>: error calling len: len of untyped nil.

In which template did you put your code? Not all the templates has access to .Data.Terms. This partial was supposed to be used on layouts/_default/terms.html. So, the site’s /tags/ or /categories/ url would show the cloud.

Yeah, division by 0 is one such case. Also, in multi-language sites, if there is no posts available under a certain language, that’s another case (though it’s very unlikely to face it, but one of my theme’s users reported it).

I see now that you wrote _"This partial is written to be used on layouts/defaults/terms.html template" when you posted the code as well. I wrote mine to be a partial I can include on any page. I haven’t seen a tag cloud as a separate page before, but I guess that is a use case as well.

Thanks for the info about the multi-language problem. My blog (which I’m build with Hugo) is English only, but as a non-native English speaker I certainly appreciate the effort to support that :+1:

https://www.sidorenko.io/post/2017/07/nice-tagcloud-with-hugo/

1 Like

Similar to Artem Sidorenko’s solution, here’s mine that uses logarithmic distribution:

My template code above is based on the old https://www.drupal.org/project/tagadelic which I have acknowledged in comments.

Can see the rendered tag cloud at:

https://worldtrip.greenash.net.au/tags/