Troliver

stories of war between boy and machine

Feeding the frontend – displaying data with D3.js Part 1

It’s been a while since I’ve posted any updates about Switchy McPortface. It works just fine in my dayjob and the data can be viewed by anyone wanting to see what is plugged into what, which has been helpful in a few situations.

However, aside from a few customised tables and functions, there’s not really a lot more that I’ve done to improve functionality. What I have done, however, is embark on making a nicer frontend with some interaction. Essentially what I am aiming to do is to display all of the computers on a single webpage for a given room, roughly corresponding to the positions that they actually exist in in each room. My first thought a few years ago was to use Unity3d or C++ and OpenGL but, since I’ve already used these things before, why not try something new and use Javascript to make a web app or something?

This guide is going to go through a nicer way to display data than just using a table generated from some basic HTML in php. There’s a few libraries out there, but I thought it would be nice to get stuck into something that has some widespread use and the name Data Driven Documents sums up nicely what I’m trying to do – generate something visual based on some underlying data. D3 gives a nice way to manipulate the DOM, bind data and provide some visualisation and I felt it fit somewhere between jQuery and (other) visualisation libraries for this project. So without further ado, here is a quick rundown of one way to use d3.js to generate something prettier than a few table rows and columns!

Starting out

Because this builds on my previous work (with the desktop client now uploaded to Github), I won’t go into exactly how you get this specific data generated, or a web server set up and running. The focus here is how to interpret data with a basic web page – so hopefully you’ll already have a way to serve and process some php files.

For this project overall, I used three files:

  • index.html – the main file you’ll call when you open your browser. It’ll also contain the page styles, instead of a separate .css file, and invoke the Javascript file
  • main.js – the file that will contain the scripts used that, in turn, call d3.js
  • query.php (this comes in part 2) – the file that handles the communication between your page and a database

The index and js files can sit at the web directory root of the server, or they can reside on your own machine. I’ve been using Brackets and testing this on my local machine, with php files running off a virtual machine and the concept is really simple:

  • Open index.html 
  • Load d3.js and the main.js files
  • Load in some data that represents computers
  • Create an SVG on the main page
  • Create a sub shape for that SVG for each computer

Index.html

The html landing page isn’t going to have an awful lot in it. Its primary purpose is to load the JS files, which in this case are d3.js v4 and jquery (used for simple AJAX calls later in part 2).

<html>
    <head>
        <title>Room test</title>
        <script src="https://d3js.org/d3.v4.js"></script>
        <script src="jquery-3.1.1.js"></script>       
    </head>
    <body>  
    </body>    
        <script src="main.js"></script>
</html>

Note that main.js is called after everything else; this is so that it loads after other JS libraries. You also don’t need to type out the entire <script type=”text/javascript” src=”main.js”></script> as of html5, since Javascript is the default script type now and the “type” attribute can be omitted.

main.js

This is where the main gubbins of this example resides. The gist of it is:

  • Create a blank SVG
  • Load in list of computers and a list of positions
  • Draw a new circle for each computer at its respective position
  • For fun, display the hostname for each of those computers when you hover over it with the mouse

Here’s each part broken down:

Creating a blank SVG

Once you’ve loaded the d3.js library in your index.html file, you can access d3 functions as in the example below. All it does is to create a blank SVG, appended to the body of the page, with an arbitrary width and height.

var w = 500;
var h = 450;
var svg = d3.select("body")
            .append("svg")
            .attr("width", w)
            .attr("height", h);

 

Load in computers and positions

So far so good? Next I’m just going to create two objects for a Computer and a Position, each taking some parameters to represent what they are. A computer is, for now, defined as simply a name – and this has a “place”. A position is a combination of a place number and its respective x and y coordinates. The reason these are separate is because at some point we might have different rooms or position layouts and I want to keep the computer and position data separate.

function Computer(place, hostname) {
    this.place = place;
    this.hostname = hostname;
}

function Position(place, posx, posy) {
    this.place = place;
    this.posx = posx;
    this.posy = posy;
}

var positions = [
    new Position('10', 0, 0),
    new Position('20', 80, 0),
    new Position('30', 160, 0),
    new Position('40', 240, 0),
    new Position('50', 0, 100),
    new Position('60', 80, 100),
    new Position('70', 160, 100),
    new Position('80', 240, 100)
];

var computers = [
    new Computer('10', "WS10562"),
    new Computer('20', "WS10239"),
    new Computer('50', "WS10555"),
    new Computer('60', "WS9111"),
    new Computer('70', "WS11032"),
    new Computer('40', "WS11031")
];

So here are two arrays of data for the computers and positions. The logic is that, whilst there may be any number of positions, they may not all be filled by a computer. Anyhow, this is all sort of irrelevant to D3 for now (and it could have been loaded in externally), so I’ll get on and demonstrate how you can now put something on screen.

Draw a new circle for each computer at its respective position

Very simply, I’m going through that array of computers and, for each one, I’ll add a circle at that position.

for (var x = 0; x < computers.length; x += 1) {
    
    var posIndex = positions.findIndex(y => y.place == computers[x].place);
                                        
    svg.append("svg")
        .append("circle")
        .attr("cx", positions[posIndex].posx + 30)
        .attr("cy", positions[posIndex].posy + 30)
        .attr("r", 20)
        .style("fill", "purple");

}

To match the data in the positions array up with the computers array, I need to first find the index of the correct item in positions array that corresponds with the “place” of the computer at the current index. In SQL, a left or an inner join would do what we need to do but in this example, I’m using two separate arrays of data that I need to match up.

Apparently, as of ES6, you can use findIndex. What this will do is return the index of positions where it finds a match by the function provided. Because I’m trying to match a property of an item in the array, the function needs to compare that “place” property for each item in the position array to the current computer’s “place” property. The => operator shortens the need to make that function by using y as the variable to represent the operative array item and it will return true when it is equal to the condition given.

For each of these computers, you can then append the SVG created earlier with a circle and give it the attributes for the radius, x and y coordinates (with an offset) and modify their style (which could be done in the index.html file but you can do it now too), which is just the fill colour here. This just adds some circles nested within the SVG tag that has been added to the page – nothing hugely complex, which is the great thing about D3. It is just a nice way to access the DOM and to add elements to a webpage dynamically.

It is important to note that this is not the best way to use D3. The way in which it should be done is to say you’ll add all of the elements of a given shape in one call then pass the data in – here, we’re going through the data first and then just adding one element on each iteration of the loop. There is no need to use a for loop in d3 and so what I’ve done is counter-intuitive to the way you’d normally learn it; but I’m simplifying the process of combining two arrays’ worth of data which I haven’t been able to find a nicer way to do when using D3. Besides, later it won’t be necessary; however, it was useful to figure out a nice way to make things work for now.

Display the hostname on mouse events

This is really easy to do. You first need to modify the previous code a little to look like this:

    svg.append("svg")
        .append("circle")
        .data(computers) //Add this!
        .attr("cx", positions[posIndex].posx + 30)
        .attr("cy", positions[posIndex].posy + 30)
        .attr("r", 20)
        .style("fill", "purple")
        .on("mouseover", fadein) //Mouse over event
        .on("mouseout", fadeout); //Mouse moved away event

Although it isn’t used as extensively as it will be later, the data function has been added in which is needed to provide the hostnames to each of the shapes when you mouse over them. For that to happen, two events need to be added with the on function – which are “mouseover” and “mouseout” (events that are triggered when the mouse is detected to have entered and exited the boundaries of the element in question). As much as I like the whole anonymous function thing in JS, I’m just going to call some named ones because I hate polluting what should be simple code with a bunch of ugly long functions that are more than 2 or 3 lines long.

And these are the functions you need:

var div = d3.select("body")
            .append("div")
            .attr("class", "tooltip")
            .style("opacity", 0)
            .text("");



function fadein(d, i) {
    div.transition().duration(200).style("opacity", 0.9);
    div.style("left", d3.mouse(this)[0])
	.style("top", d3.mouse(this)[1])
	.html(d.hostname);
    //console.log("fadeoin");
}

function fadeout() {
    div.transition().duration(400).style("opacity", 0);
    console.log("fadeout");
}

Here, the two functions can take two parameters passed into them by d3, which are the data and an index.The data is the element of the array used when each shape is created (although, in this case, that will always just be the first element, since only one circle is added at a time). The index will be which iteration that d3 element was created during.

I’ve also added a new div to the page up there too – that’s because, in order to have text pop up, I want to do it in a tiny floating “box”, which is really just an HTML element. As a result, changing the text is as simple as changing the html property of the object to whatever the hostname of that data object is. Note that, if you were to use a totally different dataset for this, “hostname” would have to be replaced by whatever else you would want to have displayed instead. I also have it appear at wherever the mouse is with a bit of a transition time, because that makes it look a bit swishier.

One last thing is that, to make it actually look nice, you might want to add a style just for the tooltip (hence div.tooltip) to the main html file within the <head> section somewhere:

<style>          
div.tooltip {
                position: absolute;
                text-align: center;
                width: 60px;
                height: 29px;
                padding: 2px;
                font: 12px sans-serif;
                background: green;
                border: 0px;
                border-radius: 9px;
                pointer-events: none;
		color: white;
            }
</style>

This centers the text and gives it a solid colour background with a bit of a rounded border and some padding.

Conclusion

If you’ve done it right, the result should look something like this (I’ve added both circles and rectangles here as a test and reduced the number of “computers” a bit for this example)

In part 2, I’ll go over how to load data in from an external data source and perhaps some more D3 stuff. If any of this is wrong, or you have any questions, please ask comments and let me know!

, , , ,

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.