Tag/Word Cloud from Any Taxonomy

Learn how to create a clickable tag/word cloud based on any taxonomy. Although you can use any taxonomy, it works best with a taxonomy that has a lot of terms. We’ll use the wordcloud2.js JavaScript library to do the cloudy bits for us. We just need to extract the tags/terms from WordPress and wrap it up in a customisable shortcode.

What We’re Going to Make

Here’s a word cloud based on the post_tag taxonomy terms for this site.

Before I started on this tutorial, I looked at several JavaScript word cloud libraries. Some were old, some were a bit naff and others were amazing. I chose to use wordcloud2.js because it’s been recently active on GitHub, it has good documentation and it doesn’t have any external dependencies. So there’s less to go wrong with future updates, and there’s less bulk to slow down your website.

You should check out the wordcloud2.js examples and download the latest source code before starting.

Getting Started

importantMake sure your site is using a custom child theme so you can keep your theme’s functions.php file tidy.

We’re going to create a directory within our custom child theme to hold the asset files, and we’ll make a main PHP file to join it all together.

Create a folder in your custom child theme’s main directory called wpt-word-cloud and place the wordcloud2.js source file in there. Create two empty text files in the same folder called word-cloud-frontend.css and word-cloud-frontend.js. You should now have a folder with three files in it – two of them being empty.

Directory listing for the WP Word Cloud tutorial
WP Tutorials – Word Cloud Folder

Back in your custom child theme’s main folder, create a new text file called wpt-word-cloud.php, paste the following “scaffold” code into it and save the file.

<?php

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');
}

function do_shortcode_wpt_word_cloud($atts) {
	$html = '';

	if (is_admin()) {
		// Don't do anything.
	} elseif (wp_doing_ajax()) {
		// Don't do anything.
	} else {
		$html .= '<p>WP Tutorials Word Cloud.</p>';
	}

	return $html;
}
add_shortcode('wpt_word_cloud', 'do_shortcode_wpt_word_cloud');

Open the your custom child theme’s functions.php file and add the following to it:

/**
 * WP Tutorials Word Cloud (WPTWC)
 */
require_once 'wpt-word-cloud.php';

In your website, edit the content where you want to display your word cloud, add the shortcode, save the content and then view it. You should see the words “WP Tutorials Word Cloud” come through to the frontend of your site.

WP Tutorials Word Cloud shortcode
The Word Cloud Shortcode

That’s all we need to do to kick things off. We’ve got a main PHP file where we can put most of our code (wpt-word-cloud.php), we’ve linked to it from the theme’s functions.php file, and we’ve got a folder where we can stash our assets.

The Core PHP Logic

Before we write the main PHP function that’s going to do the back-end work, we need to figure out how it’s actually going to work. The format of these shortcode tutorials that rely on an external JavaScript library is pretty standard:

  • Enqueue assets that the frontend/browser is going to need – CSS and JavaScript files.
    • They could be our own assets, or they could be assets provided by an external library.
  • Grab some data from WordPress and sort it, tidy it, or structure it into an array.
  • Set a “data-” property of an HTML element with our structured array.
  • Add a bit of frontend JavaScript to find our HTML element, extract the “data-” property, and pass it all to the JS library.

The thing that makes each of these shortcode tutorials different is how we grab and format the WordPress data before we stick it in the “data-” property and process it in the frontend.

In this case, the documentation for wordcloud2.js says we need to create a word list array, in this format:

[
[word, size, data1, data2, ... ],
[word, size, data1, data2, ... ],
[word, size, data1, data2, ... ],
...
]

…where “size” is a font size and data1/data2/… are anything we want – we’re going to pass some URLs in there.

We’re also going to want to have our largest words at the start of the array, and the smallest words at the end.

We can’t just put the word counts into the “size” field, because if a word has a count of 153, then the frontend will try to render that word with a font size of 153pt. Way too big. So we also need to take the word counts and scale them up/down to a sensible range of font sizes. That means… some maths.

From experience, passing arrays in “data-” properties can be a bit funky – the browser might decode them as an object, rather than as an array. So we’re going to pass three objects to the frontend. One object will contain the general options for the word cloud, another will hold the word list, and the final object is just for some misc settings. When we pick these up in the browser (word-cloud-frontend.js) we’ll convert the word-list-object into an array of the correct format for wordcloud2.js.

The Main PHP Code

Here’s the big code-lump. Copy and paste this into wpt-word-cloud.php. Read through the comments and you’ll see the sections that we just covered in the logic section.

<?php

/**
 * WP Tutorials Word Cloud (WPTWC)
 *
 * https://wp-tutorials.tech/refine-wordpress/word-tag-cloud/
 *
 */

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');
}

const WPTWC_DEFAULT_MIN_FONT_SIZE = 16;
const WPTWC_DEFAULT_MAX_FONT_SIZE = 60;
const WPTWC_DEFAULT_TAXONOMY = 'post_tag';
const WPTWC_DEFAULT_GRID_SIZE = 15;
const WPTWC_DEFAULT_ROTATE_RATIO = 0.25;
const WPTWC_DEFAULT_ROTATION_STEPS = 2;

const WPTWC_WEIGHT_THRESH_HIGH = 0.60;
const WPTWC_WEIGHT_THRESH_MED_HIGH = 0.30;
const WPTWC_WEIGHT_THRESH_MED_LOW = 0.05;

function wpt_enqueue_wordcloud_assets_if_not_already_enqueued() {
	$base_url = get_stylesheet_directory_uri() . '/' . pathinfo(__FILE__, PATHINFO_FILENAME) . '/';
	$version = wp_get_theme()->get('Version');

	wp_enqueue_script('wordcloud2', $base_url . 'wordcloud2.js', array('jquery'), '2.0.3', true);
	wp_enqueue_script('wpt-word-cloud', $base_url . 'word-cloud-frontend.js', array('wordcloud2'), $version, true);

	wp_enqueue_style('wpt-word-cloud', $base_url . 'word-cloud-frontend.css', null, $version);
}

function do_shortcode_wpt_word_cloud($atts) {
	$html = '';

	if (is_admin()) {
		// Don't do anything.
	} elseif (wp_doing_ajax()) {
		// Don't do anything.
	} else {
		wpt_enqueue_wordcloud_assets_if_not_already_enqueued();

		$args = shortcode_atts(
			array(
				'taxonomy' => WPTWC_DEFAULT_TAXONOMY,
				'min_font' => WPTWC_DEFAULT_MIN_FONT_SIZE,
				'max_font' => WPTWC_DEFAULT_MAX_FONT_SIZE,
				'gridSize' => WPTWC_DEFAULT_GRID_SIZE,
				'rotateRatio' => WPTWC_DEFAULT_ROTATE_RATIO,
				'rotationSteps' => WPTWC_DEFAULT_ROTATION_STEPS,
				'class' => '',
			),
			$atts
		);

		// Extract the terms from our taxonomy. This is an array of WP_Term
		// objects. Each WP_Term object has a name, slug and count.
		$terms = get_terms(array(
			'taxonomy' => $args['taxonomy'],
			'hide_empty' => true,
		));

		$words = array();
		$min_count = PHP_INT_MAX;
		$max_count = 0;
		if (is_array($terms)) {
			foreach ($terms as $term) {
				$key = sprintf('%08d_%s', $term->count, $term->name);

				$words[$key] = array(
					'slug' => $term->slug,
					'text' => $term->name,
					'count' => $term->count,
					'link' => get_term_link($term, $args['taxonomy']),
				);

				if ($term->count < $min_count) {
					$min_count = $term->count;
				}

				if ($term->count > $max_count) {
					$max_count = $term->count;
				}
			}

			krsort($words);
		}

		// The maths bit - scale the term counts to font sizes.
		$count_range = ($max_count - $min_count);
		$min_font_size = $args['min_font'];
		$max_font_size = $args['max_font'];
		$font_size_range = ($max_font_size - $min_font_size);
		foreach ($words as $key => $word) {
			$percentage = 1.0 * ($words[$key]['count'] - $min_count) / $count_range;
			$words[$key]['weight'] = round($min_font_size + ($percentage * $font_size_range));
		}

		// Options for wordcloud2.js
		$wordcloud_options = array(
			'gridSize' => $args['gridSize'],
			'rotateRatio' => $args['rotateRatio'],
			'rotationSteps' => $args['rotationSteps'],
			'drawOutOfBound' => false,
			'shrinkToFit' => true,
		);

		// Other bits and bobs we want to pass to the frontend.
		$misc_options = array(
			'maxWeight' => $max_font_size,
			'minWeight' => $min_font_size,
			'weightThresholdHigh' => round($min_font_size + (WPTWC_WEIGHT_THRESH_HIGH * $font_size_range)),
			'weightThresholdMedHigh' => round($min_font_size + (WPTWC_WEIGHT_THRESH_MED_HIGH * $font_size_range)),
			'weightThresholdMedLow' => round($min_font_size + (WPTWC_WEIGHT_THRESH_MED_LOW * $font_size_range)),
		);

		// CSS classes for the word cloud container div.
		$classes = array('wpt-word-cloud');
		if (!empty($args['class'])) {
			$classes[] = sanitize_title($args['class']);
		}

		$html .= sprintf(
			'<div class="%s" data-word-cloud-words="%s" data-word-cloud-options="%s" data-word-cloud-misc="%s" style="opacity: 0.0;"></div>',
			esc_attr(implode(' ', $classes)),
			esc_attr(json_encode($words)),
			esc_attr(json_encode($wordcloud_options)),
			esc_attr(json_encode($misc_options))
		);
	}

	return $html;
}
add_shortcode('wpt_word_cloud', 'do_shortcode_wpt_word_cloud');

The main do_shortcode_wpt_word_cloud() function breaks down like this:

  1. Enqueue the assets, including wordcloud2.js.
  2. Use shortcode_atts() to parse the attributes passed to the shortcode, so the $args array holds our sanitised parameters.
  3. Create the $options array to pass to wordcloud2.js in the frontend.
  4. Call get_terms() to grab all the terms we want, as an array of WP_Term objects.
  5. Loop through $terms
    1. Create a key for each term, based on its word count and the word itself. Example: 00000023_Some Term Name
    2. Add the term to our $words array.
  6. Use krsort() to reverse-sort $words by the keys that we created, so that the most-used words are first in the array.
  7. Loop through $words
    1. Assign a font size (weight) to each word, based on its word count.
  8. Render the <div> element
  9. Set data-word-cloud-words property to hold a JSON Encoded version of $words.
  10. Set the data-word-cloud-options property to hold a JSON Encoded version of $options.
  11. An Opacity of zero on the <div> will make it transparent. We’ll fade it in after wordcloud2.js has rendered the words.

Our Frontend JavaScript Code

The frontend code has to do a couple of things, but the logic is pretty simple, even if the jQuery looks a bit cryptic at first:

  1. We check WordCloud.isSupported to see if wordcloud2.js is happy that all prerequisites have been met.
  2. For each HTML element which has a data-word-cloud-words property…
    1. Grab data-word-cloud-words and turn it into an array that’s in the correct format for wordcloud2.js
    2. If we have more than zero words…
      1. Determine some threshold weights so we can assign different CSS classes to words in different weight bands.
      2. Grab data-word-cloud-options and add some additional options. These came from the API documentation for wordcloud2.js:
        1. click handler
        2. classes handler.
        3. Add a handler for the wordcloudstop event, which is raised after the word cloud has drawn itself, so we can set the opacity to 1.0 (100%).
      3. Finally… call the main WordCloud() function and let wordcloud2.js do the actual hard work for us.

Copy and paste the following into wpt-word-cloud/word-cloud-frontend.js and save it.

/**
 * WP Tutorials Word Cloud - Frontend
 * 
 * https://wp-tutorials.tech/refine-wordpress/word-tag-cloud/
 *
 * Thanks to wordcloud2.js: https://github.com/timdream/wordcloud2.js
 */
(function($) {
    'use strict';

    $(window).on('load', function() {
        // console.log('WP Tutorials Word Cloud : load');

        if (WordCloud.isSupported) {
            $('[data-word-cloud-words]').each(function(index, value) {
                // Get our words object.
                var wordsObject = $(this).data('word-cloud-words');

                // Convert wordsObject into an array that wordcloud2 can use.
                var wordsArray = Object.keys(wordsObject).map(function(key, index) {
                    return [
                        wordsObject[key].text,
                        wordsObject[key].weight,
                        wordsObject[key].link
                    ];
                });

                // Uncomment to see what the word array looks like.
                // console.log(wordsArray);

                // Only proceed if we've actually got some words in wordsArray.
                if (wordsArray.length > 0) {
                    // Get the weight CSS class thresholds,
                    // and any other misc. options.
                    var miscOptions = $(this).data('word-cloud-misc');

                    // get the data-word-cloud-options prop.
                    var options = $(this).data('word-cloud-options');

                    // Add the actual words.
                    options.list = wordsArray;

                    // The click-on-word handler.
                    options.click = function(item) {
                        var word = item[0];
                        var size = item[1];
                        var url = item[2];

                        // Uncomment this to see some diagnostics.
                        // console.log('Word=' + word + ' :: Size=' + size + ' :: URL=' + url);

                        // Comment these if you want to stop following clicks.
                        if (url) {
                            location.href = url;
                        }
                    };

                    // Assign CSS classes to the words.
                    options.classes = function(word, weight, fontSize, distance, theta) {
                        if (weight >= miscOptions.weightThresholdHigh) {
                            return 'high-weight';
                        } else if (weight >= miscOptions.weightThresholdMedHigh) {
                            return 'medium-high-weight';
                        } else if (weight >= miscOptions.weightThresholdMedLow) {
                            return 'medium-low-weight';
                        } else {
                            return 'low-weight';
                        }
                    };

                    // When the rendered has finished, set the opacity to 1.0
                    // to fade it in.
                    $(this).on('wordcloudstop', function() {
                        $(this).css({ opacity: '1.0' });
                    });

                    // Get the containing <dov> element.
                    var element = $(this)[0];

                    // Call the main wordcloud2.js function - WordCloud().
                    WordCloud(element, options);
                }
            });
        }

    });
})(jQuery);

That’s it… pretty-much. If you go to the content that has your shortcode and do a forced/full reload (bypass the cache) then you should see a word cloud… sort of… maybe… probably not.

Our <div> element has no height yet, so the word cloud is working, but the DOM element needs some CSS properties so we can actually see it.

Add Some Style

Copy and paste the following into wpt-word-cloud/word-cloud-frontend.css and save it.

/*
 * WPT Word Cloud
 * https://wp-tutorials.tech/refine-wordpress/word-tag-cloud/;
 */
.wpt-word-cloud {
    position: relative;
    border-radius:  2em;
    width:  100%;
    height:  25em;
    transition: opacity 0.50s;
    background-color:  transparent !important;
    margin-top:  1em;
    margin-bottom:  1em;
}

.wpt-word-cloud > span {
    transition:  0.3s;
}

.wpt-word-cloud > span:hover {
    color:  #ff8c00 !important;
    cursor: pointer;
}

.wpt-word-cloud .high-weight {
    color:  #1700cc !important;
}

.wpt-word-cloud .medium-high-weight {
    color:  #1700ccc0 !important;
}

.wpt-word-cloud .medium-low-weight {
    color:  #1700cca0 !important;
}

.wpt-word-cloud .low-weight {
    color:  #1700cc80 !important;
}

Save, Test & Deploy the Tag Cloud

That’s it! Make sure everything is saved, go to your site’s content and do a full reload, bypassing the browser’s cache. You should now see an interactive tag/word cloud! Check your JavaScript console to see if there are any errors.

Test the Shortcode Options

Shortcode options, parsed by shortcode_atts().

OptionDefaultNotes
taxonomy“post_tag”The taxonomy that we want to grab terms for.
min_font16Minimum font size used when scaling term counts to font sizes.
max_font60Maximum font size used when scaling term counts to font sizes.
gridSize15Controls the spacing between the words when they’re rendered. A bigger number gives more spacing.
rotateRatio0.25The probability that any given word is rotated. Set to zero to have all words horizontal. Set to 1.00 to make all words rotated.
rotationSteps2How many rotation orientations are there.
class“”A list of space-separated CSS class names to pass to the <div> container.
Shortcode options for the word cloud

This example will render a word cloud of all the post categories on a site, and none of the words will be rotated 👉 👉 👉

Have fun with your word clouds 😎

Tag cloud shortcode options
Example Word Cloud Options

Leave a Comment

Your email address will not be published. Required fields are marked *