Build

Creating WordPress Site Visualization Plugin with PHP

10 min read Michael Carroll on Jul 14, 2016

In this blog post we are going to demonstrate how to embed PubNub inside WordPress to enable some cool real-time features within the WordPress platform. We will be building a plugin which can create a visualization of all the content within a WordPress site, and of course it will be able to update itself in real time.

Since WordPress platform is based on PHP, we are going to use the PubNub’s PHP SDK in this demo. The plugin is called SiteViz , short for ‘Site Visualization’, and it creates a tree like visualization of the content, starting from categories, to posts and their comments.

wordpress visualization plugin demo

 

The SiteViz plugin offers a bird’s eye view of the entire site and its contents. If you are a site administrator managing a content website, then you have certain challenges in managing your site. And as the content increases, you have to deal with information overload. This is where a plugin like SiteViz can help you by visually portraying the site content and showing some essential metrics, such as, number of posts in draft/pending state, posts per category, popular posts, number of comments unapproved/approved etc. In essence, SiteViz can portray in one picture what might otherwise take you hundreds of lines of scrolling and searching.

Note: – This plugin is created for demo purpose and not tested on production WordPress site.

 

WordPress Plugin Project Overview

The WordPress platform offers a bunch of hooks, in the form of actions, which can be triggered along with user performed events. For example: a site visitor opening a post, an author submitting a post for review, an editor publishing a post, or a random visitor commenting on a post. All these events can trigger hooks, which are nothing but PHP functions that can be called and executed within the WordPress platform.

For building this plugin, we have tapped into some of the WordPress action hooks which allow us to capture the state of the site content and publish it over PubNub so that the updates can reflect in the visualization. We used the following action hooks:

  • ‘admin_menu’ – For adding an additional admin menu for the plugin
  • ‘edit_post’ – For publishing post details of a new or modified post
  • ‘edit_comment’ – For publishing comment edits
  • ‘transition_comment_status’ – For publishing changes in comment states
  • ‘trash_comment’ – For publishing comment deletion

To enable realtme updates in the visualization, we have used the PubNub PHP SDK and Javascript SDK.

The complete source code of SiteViz is available on GitHub in this repo.

WordPress Setup

Setting up the plugin can be achieved in three easy steps.

1. Import the plugin to WordPress

First, copy the content of the repository folder path ‘/wordpress/wp-content/plugins/ to /wp-content/plugins/ within your WordPress installation.

2. Install and activate the Plugin

Log on to your admin dashboard and click on the plugins menu. You will see that SiteViz is listed in the list of plugins. Click on the Activate link below the plugin name.

wordpress visualization plugin GUI

3. Plugin Settings

Once activated, you can access the plugin settings UI from the Settings > SiteViz menu.

wordpress visualization plugin GUI

Enter your PubNub Subscribe and Publish keys in the settings form and press ‘Submit’ to save the SiteViz settings in your WordPress installation.

Accessing the Plugin

Once the plugin is activated and setup as presented in the previous section, you can access the main plugin UI by activating the ‘SiteViz’ menu within the main menu of your WordPress dashboard.

wordpress visualization plugin GUI

The above figure is only for illustration purposes. The actual visualization displayed by the plugin will depend on how many categories, posts and comments exist in your site. You can create a few of them after installing the plugin to see them under the visualization.

The visualization consists of several icons to represent the various content elements in the site and connecting lines to indicate a relationship between them.

Based on this, the categories will appear at the top as blue circle icon wp plugin category icon.

At the next level, the posts will appear as a document icons wp plugin post icon, along with a line connecting them to their respective categories. The post icons can be in three colors. Gray signifies that the post is in draft stage, cyan signifies that the post is in pending and green signifies that the post is published.

Similarly, all posts that have comments will have a light orange line linking them to their respective comments represented by a speech bubble icon wp plugin comment icon. This icon can have two colors. Gray indicates unapproved comment and orange indicates otherwise.

Hovering the mouse over the icons will display a tooltip revealing more details about the corresponding content element.

PubNub Initialization

In order to push the real-time updates about the site content changes, the WordPress plugin initializes the PubNub PHP SDK as follows.

//PHP PubNub Object Initialization
$pubnub = new Pubnub($pubnub_pub_key, $pubnub_subs_key);

Similarly, for receiving the real-time updates and updating the client side visualization, we need to use the PubNub JavaScript Library.

var pubnub = PUBNUB({
    subscribe_key: '<?php echo $pubnub_subs_key; ?>', // always required
    publish_key: '<?php echo $pubnub_pub_key; ?>'    // only required if publishing
});
// Subscribe to a channel
 pubnub.subscribe({
    channel: '<?php echo $pubnub_chanel_name; ?>',
    message: function(m){
        
        if(viz){
            viz.refresh(m);    
        } else {
            loadAllPostData();
        }
        
    },
    error: function (error) {
      // Handle error here
      alert('Error'+JSON.stringify(error));
    }
 });

The JavaScript initialization code is also embedded inside PHP but will run on the browser once the admin user activate and accesses the Plugin.

SiteViz Initial Loading

Once the plugin is loaded, it makes an AJAX call to the server and gets all the categories, posts and comments in a JSON format. This is used to initialize a Javascript object named VizElement, which manages the entire scene for this visualization.

/*
 * VizElement Object for Managing the SiteViz SceneGraph. Based on D3js Force Layout
 * @param parent_elem  - ID of container element
 * @param inputJson    - JSON dump of the existing site strucure for initialization
 * @param resourcePath - Internal path of the image resources within wordpress 
*/
var VizElement = function(parent_elem,inputJson,resourcePath) {
    
    // load in arguments from config object
    var that = this;
    this.data = inputJson;
    this.pelement = parent_elem;
    this.resPath = resourcePath + '/images/';
    this.nodes = new Array();
    this.links = new Array();
    
    this.categoryCheck = new Array();
    this.postSVG = null;
    this.commentSVG = null;
    d3.xml(this.resPath + "post.svg", "image/svg+xml", function(error, xml) {
    if (error) throw error;
    that.postSVG = document.importNode(xml.documentElement, true);
    d3.xml(that.resPath + "comment.svg", "image/svg+xml", function(error, xml) {
      if (error) throw error;
      that.commentSVG = document.importNode(xml.documentElement, true);
      // create the chart
        that.derive();
    	  that.draw();
 	  
    });
  });
}

SiteViz Rendering

The rendering of visualization is achieved using the popular D3js JavaScript library. D3 has a built-in layout for creating force-directed graphs that we have used here for this plugin. Force-directed graphs represent a bunch of nodes interconnected with links and are best suited for portraying a graph like relationship between entities. They can be simulated like the laws of physics to emulate the forces exerted by physical bodies on one another.

For this plugin, we have chosen a force directed graph with a gravity of 0.5 and a charge of -240 so that all the elements will appear to be pulled down and be repelled by one another.

VizElement.prototype.draw = function(){
  var that = this;
  //Setup Dimensions
  this.width = this.pelement.offsetWidth - 100;
  this.height = 600;
  this.linearScale = d3.scale.linear().domain([0,10000]).range([15,50]);
  //Initialize SVG
  this.pelement.innerHTML = '';
    this.svg = d3.select(this.pelement).append('svg');
    this.svg.attr('width',  this.width);
    this.svg.attr('height', this.height);
    this.linkg = this.svg.append('g').attr('class','link-group');
    this.nodeg = this.svg.append('g').attr('class','node-group');
    calculateCategorySpacing(this);    
    //Set up tooltip
    
  this.tip = d3.tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html(function (d) {
      
      	var author = d.type == "post" ? d.data.post_author_name : d.data.comment_author;
      	var date   = d.type == "post" ? d.data.post_date : d.data.comment_date;
      	var content = d.type == "post" ? d.data.post_title : d.data.comment_content;
      	if(d.type == "post"){
      		var htmlString ='<div style="padding:5px;">';
      		htmlString +=  "<div>" +  "Post Author : " + author + '</div>';
      		htmlString +=  "<div>" +  "Post Date: " + date + '</div>';
      		htmlString +=  '<div>' + "Post Title : " + content + '</div>';
      		htmlString +=  "</div>";
      		return  htmlString;
      	}
      	if(d.type == "comment"){
      		
      		var htmlString = '<div style="padding:5px;">';
      		htmlString +=  "<div>" + "Comment Author : " + author + '</div>';
      		htmlString +=  "<div>" +  "Comment Date: " + date + '</div>';
      		htmlString +=  '<div class="comment-wrap">' + "Comment : " + content + '</div>';
      		htmlString +=  "</div>";
      		return  htmlString;	    		
      	}
      	if(d.type == "category"){
      		return  '<div style="padding:5px;">'  + "Category Name : " + d.data + '</div>';
      	}
  })
  
  this.svg.call(this.tip);
  //Initialize Force Layout
    this.force = d3.layout.force()
    	.nodes(this.nodes)
    	.links(this.links)
    	.gravity(0.5)
    	.charge(-240)
    	.linkDistance(function(d){
    		return d.type == 0 ? 50 : 5;
    	})
    	.size([this.width, this.height]);
    this.fnode = this.force.nodes();
    this.flink = this.force.links();
    //Start force layout display
    update(this); 	
}
function update(obj) {
  //Setup Links
  var link = obj.linkg.selectAll(".link")
    			.data(obj.links,function(d){
    				return d.key ;
    			});
    link.enter().append("line")
      .attr("class", "link")
      .style("stroke-width", function(d) { 
      	return d.type == 0 ? "0.5" : "0.25" ; 
      })
      .style("stroke", function(d) { 
      	return d.type == 0 ? "blue" : "orange" ; 
      });
      link.exit().remove();
    //Setup Nodes
    var node = obj.nodeg.selectAll(".node")
      .data(obj.nodes,function(d){
      	if(d.type == "post"){
      		return "post-"+d.data.pid;
      	} else if (d.type == "comment"){
      		return "comment-"+d.data.comment_id;
      	} else if (d.type == "category"){
      		return "category-"+d.data;
      	}
      });
    var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .each(function(d,i){
      // Clone and append xml node to each data binded element.
      if(d.type == "post"){
      	this.appendChild(obj.postSVG.cloneNode(true));			  	
      	d3.select(this).select('svg').attr("x", -10)
          .attr("y", -300)
          .attr("width", function(d){
          		return obj.linearScale(d.data.post_count + 1);
          })
      } else if(d.type == "comment"){
      	
      	this.appendChild(obj.commentSVG.cloneNode(true));	
      	d3.select(this).select('svg').attr("x", -5)
          .attr("y", -300)
          .attr("width", function(d){
          		return 10;
        })
      }
      
    })
      .call(obj.force.drag)
      .on('mouseover', obj.tip.show) 
 	  .on('mouseout', obj.tip.hide); 
 	  node.exit().remove();
 	  colorize(node,-1);
 	  //Setup the tick function 
 	  obj.force.on("tick", function() {
      
      link.attr("x1", function(d) { return d.source.x; })
          .attr("y1", function(d) { return d.source.y; })
          .attr("x2", function(d) { return d.target.x; })
          .attr("y2", function(d) { return d.target.y; });
       
    node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });		
    
    });
 	  
 	  //Start Force Layout simulation
 	  obj.force.start();
}

 

SiteViz Dynamic Refresh

Every time a new post is added or an existing post is modified or deleted, the hooks registered by SiteViz plugin is activated to capture the details of the post and is published on a channel, as per the following JSON format.

if(refreshObj.action == "New"){
      //New Post added to the wordpress site
      addCategory(this,refreshObj.records[0]);
      addPost(this,refreshObj.records[0]);
    
} else if(refreshObj.action == "Modify") {
      //existing post modified in the wordpress site
      modifyPost(this,refreshObj.records[0]);
  
} else if (refreshObj.action == "Delete") {
      //Existing post deleted in wordpress site
      deletePost(this,refreshObj.records);
}
{
  "result": "Yes",
  "action": "New",
  "records": [{
    "id": 20,
    "pid": 70,
    "post_status": "publish",
    "post_author": 1,
    "post_author_name": "root",
    "user_login": "root",
    "post_date": "2016 - 06 - 09 04: 18: 49 ",
    "post_count": 0,
    "post_title": "root dummy post",
    "post_name": "root - dummy - post",
    "tags": " ",
    "categories": "demo - cat "
  }]
}

The WordPress post details are sent within the “records” parameter contained in JSON data and are self explanatory. The two top level parameters “result” and “action” signify presence of post record and type of post ( whether it is a new post, or modified or to be deleted post).

Similarly, every time a new comment is added or an existing comment is transitioned from unapproved state to approved state, or vice versa, another action hook is activated

handleComment = function(obj,commentObj){
  var comment = commentObj.commentsAndMeta.comment[0];
  var foundComment = false;
  var i = 0;
  while(i < obj.links.length){		
    if((obj.links[i].type == 1) && (obj.links[i].target.data.comment_id == comment.comment_id) ){
      foundComment = true;
      break;
    }
    i++;
  }
  if(foundComment){
    obj.links[i].target.data = comment;
  } else {
    var j = 0;
    var foundPost = false;
    while(j < obj.nodes.length){
      if((obj.nodes[j].type == "post") && (obj.nodes[j].data.pid == comment.comment_post_id) ){
        foundPost  = true;
        break;
      }
      j++;
    }
    if(foundPost){
      obj.nodes.push({"type" : "comment" , "data" : comment })
      obj.links.push({"source" : j , "target" : obj.nodes.length - 1 , "type" : 1 , "ref_post_id" : comment.comment_post_id, "key" : obj.nodes[j].data.pid + '-' + comment.comment_id});
    }		
  }
}

And it publishes the comment in the following JSON format.

{
  "result": "Yes",
  "records": [{
    "Type": "Comments",
    "commentsAndMeta": {
      "comment": [{
        "comment_post_id": "52",
        "comment_content": "here is my comment",
        "comment_author": "root",
        "comment_author_email": "getto@siteviz.com",
        "comment_date": "2016-06-05 17:13:23",
        "comment_date_gmt": "2016-06-05 17:13:23",
        "comment_approved": "1",
        "neg": "",
        "neutral": "",
        "pos": "",
        "label": ""
      }]
    }
  }]
}

The comment details are sent in the “comment” parameter under the “records” array in JSON. All comment parameters are as per the standard data captured for every WordPress comment, except for the last four parameters, namely, “neg” , “neutral” ,”pos” , “label”. These parameters signify the sentiment score of the comment but are not used currently, hence marked as blank.

On the plugin visualization side, all dynamic updates will result in refreshing the force directed graph in a few different ways. The refresh( ) function in VizElement handles all the dynamic updates.

VizElement.prototype.refresh = function(msgObj){
  var that = this;
  var refreshObj = JSON.parse(msgObj); 
  if('action' in refreshObj)
  {
    //Checking for an existing post in draft state.
    //In draft state it will be always reported as New , even if there is a modificaition to an existing post
    //Hence an existing draft post with action "New" should be marked as "Modify" 
    if((refreshObj.action == "New") && this.doesPostExist(refreshObj.records[0])){
      refreshObj.action = "Modify";
    }
    //Post Handling
    if(refreshObj.action == "New"){
      //New Post added to the WordPress site
      addCategory(this,refreshObj.records[0]);
      addPost(this,refreshObj.records[0]);
    
    } else if(refreshObj.action == "Modify") {
      //existing post modified in the WordPress site
      modifyPost(this,refreshObj.records[0]);
    } else if (refreshObj.action == "Delete") {
      //Existing post deleted in WordPress site
      deletePost(this,refreshObj.records);
    }
  } else {
    //Comment update Handling
    handleComment(this,refreshObj.records[0]);
  }
  update(this);
  console.log(this.links);
}

 

Conclusion

WordPress is a very popular content management system, but with an increasing number of content and contributors it can really clutter the admin’s workflow and can become a challenge to manage the site effectively. One of the ways of reducing the clutter is to create a beautiful visualization like SiteViz that can provide instant answers to some of the day to day questions, like “How many posts are pending review ?“.  Adding real-time updates can provide further ease by reducing the number of clicks or page refreshes the admin has to perform.

There is already a large number of WordPress plugins that improve productivity or simplify the workflows of site management. However, most of them do not have any real-time notification or update capabilities. We hope that this plugin can pave the way for building super responsive plugins with PubNub which can make the lives of site administrators easier and less cumbersome. Beyond this, plugins also interact with the site pages, so there is also a potential to use PubNub to enable some kind of interaction with the site visitors, for example, chat . The possibilities are many, and with PubNub it is not hard to realize them.

0