Create a Data Marvel — Part 9: Building the Webpage for Comics

Create a Data Marvel — Part 9: Building the Webpage for Comics

We now reached the point to create the webpage for interacting with the Marvel comic data! Through the past 8 blog posts, we have seen how to start a development project from scratch — from a data set to a functioning application interacting with the data. It is this step where we build a prettier user interface that allows users to search for comics, see their details, and interact with a graph visualization of the data.

To catch up with the most recent post of this series where we built the endpoints for retrieving comic issues and related entities, you can read Part 8. Let’s get to it!

Creating the HTML page

There is quite a bit of code that goes into the issues.html file in this project, so we will show some (but not all!) of the code for that file in this post. We will walk through the bits of code that handle and render our data, then spend a bit of time on the visualization section of the page. Of course, the full code can be found on our GitHub repository. Feel free to check that out!

First, let’s get a feel for what the end result will look like in the image below.

Pretty cool, right?! Let’s find out how to build it!

Handling the list of comics

First, let’s start by analyzing the search functionality and the populated list of comics that show up in the pane on the left-hand side of the page, which is in the image below.

The code retrieves comics based upon the values users put in the search box at the top. Most of the code is contained in a search() function and then added to the html content on our page.

function search() {
var query = $(“#search”).find(“input[name=search]”).val();
$.get(“/comicissues/findbynamelike?name=*” +
encodeURIComponent(query) + “*”,
  function (data) {
var t = $(“table#results tbody”).empty();

if (!data) return;
data.forEach(function (issue) {
$(“<tr><td class=’issue’>” + issue.name +
“</td><td>” + issue.issueNumber +
“</td><td>” + issue.pageCount +
“</td></tr>”).appendTo(t)
.click(function () {
showIssue($(this).find(“td.issue”).text());
})
});
    showIssue(data[0].name);
}, “json”);
return false;
}

The starting line inside the function finds the value of the search input box and sets that to the query variable. The next line hits our fuzzy search endpoint query to search our database for comics with a name like the value in our search box. The next few lines call another function to loop through the results and create the html tag to encompass each value. Those dynamically-generated <tr> and <td> tags get inserted into the below html block.

<div class=”panel panel-default”>
<div class=”panel-heading”>Search Results</div>
<table id=”results” class=”table table-striped table-hover”
<thead>
<tr>
<th>Name</th>
<th>Issue Number</th>
<th>Page Count</th>
</tr>
</thead>
<tbody>
//new, dynamic content tags will go here!
</tbody>
</table>
</div>

Great! That takes care of our list of search results. I set my default search input box text to “Captain America” to retrieve those comics (he’s my favorite character!), but you can default that value to anything you like in the <input value="Captain America"> tag. :) A default value ensures you see some values and not empty results when you first load the webpage.

Next, we will cover the top panel on the right to show the details of each comic as it’s selected.

Displaying ComicIssue Details

In this section of the page, we want to show how we pulled each section of content (Characters, Creators, Series, Stories, Events, plus the ComicIssue image) and display it on our webpage. The following code is for the part of the page shown below.

Similar to what we saw above, we use a JavaScript function (showIss()) to retrieve the values from our database and dynamically generate the surrounding html tags. That content then gets inserted between html content sections where we see the nice blocks of bulleted lists. Let’s take a look.

function showIssue(name) {
$.get(“/comicissues/findbyname?name=” + encodeURIComponent(name),

function (data) {
if (!data) return;

var issue = data;
$(“#name”).text(issue.name);
$(“#poster”).attr(“src”, issue.thumbnail);
    var $charList = $(“#characters”).empty();
issue.characters.forEach(function (issueChar) {
var person = issueChar.name;
var thumbnail = issueChar.thumbnail;
$charList.append($(“<li><img src=’” + thumbnail +
“‘ id=’comicChar’ style=’height: 50px; width: auto’/> “ +
person + “</li>”));
});
    var $creList = $(“#creators”).empty();
issue.creators.forEach(function (issueCre) {
//similar code here for creators
});
    var $seriesList = $(“#series”).empty();
issue.series.forEach(function (issueSeries) {
//similar code here for series
});
    var $storyList = $(“#stories”).empty();
issue.stories.forEach(function (issueStor) {
//similar code here for stories
});
    var $eventList = $(“#events”).empty();
issue.events.forEach(function (issueEvent) {
//similar code here for events
});
    render();
}, “json”);
return false;
}

In the function call on the first line, we pass in the name of the ComicIssue, which is whatever comic the user clicks on from the left-side pane we talked about above. Each time the user clicks a different comic from the list on the left, this function gets called and retrieves the relevant content for that comic name. The first line within the function simply calls the findByName() method from our application. We verify we received data back from the call, and then set that data to the issue variable.

Next, we set the name (though we currently don’t display that) and poster values. For the poster, notice that we use the image url from our node in the database as the src property on the html tag. This allows the tag to retrieve that image from the url and display it on the page!

The next blocks of code loop through each of the nested JSON structures that contain the related Characters, Creators, Events, Series, and Stories that are connected to the chosen ComicIssue. We will take a closer look at these blocks below for clarity.

var $charList = $(“#characters”).empty();
issue.characters.forEach(function (issueChar) {
var person = issueChar.name;
var thumbnail = issueChar.thumbnail;
$charList.append($(“<li><img src=’” + thumbnail +
“‘ id=’comicChar’ style=’height: 50px; width: auto’/> “ +
person + “</li>”));
});

In the above segment, we set a variable $charList to the empty contents of the html tag with an id of characters. Then, we loop through our nested JSON of Characters for that ComicIssue (issue.characters.forEach). For each Character object in the list, we set the character name and character thumbnail. Finally, we append to our $charList variable, adding the values wrapped in the appropriate html tags.

You might have noticed that certain images do not render for the Series section, and some of the Story values might seem meaningless. This is due to bad values coming from the API that were then transferred to our database. I think there should be some data cleanup discussions and activity from the owners of the API. However, since that is not something we can control, we could determine how to clean up the data in the database or render bad data in a friendlier manner on the front end. Either way, we have tabled that discussion for later, as it could be a topic of its own!

This same process occurs for each of our other lists (Creator, Event, Series, and Story), and then triggers our render() event, which we will discuss in the next section.

Rendering the Data in a Graph Visualization

Our last component of the webpage is the graph visualization, representing our nodes and relationships from our Neo4j database as circles and lines. This visualization is rendered using the D3 library, which we discussed in the last post for getting our graph data into the Map<> format D3 expects. This piece of our webpage is where all those pieces come together!

function render() {

d3.select(“svg”).remove();
var width = 500, height = 500;
var force = d3.layout.force().charge(-200)
.linkDistance(30).size([width, height]);
  var svg = d3.select(“#graph”).append(“svg”)
.style(“width”, “100%”).style(“height”, “100%”)
.style(“background-color”, “white”)
.attr(“pointer-events”, “all”);
  d3.json(“/comicissues/buildgraph”, function (error, graph) {
if (error) return;
    force.nodes(graph.nodes).links(graph.links).start();
var link = svg.selectAll(“.link”)
.data(graph.links).enter()
.append(“line”)
.attr(“class”, “link”);
var node = svg.selectAll(“.node”)
.data(graph.nodes).enter()
.append(“circle”)
.attr(“class”, function (d) {
return “node “ + d.label
})
.attr(“r”, 10)
.call(force.drag);
    //html title attribute
node.append(“svg:title”)
.text(function(d) {
return “Type: “ + d.label + “\nName: “ + d.name;
});
node.append(“name”)
.text(function (d) {
return d.name;
});
    ....
});
}

The first line inside our render() function ensures the field is cleared out and no residual data is in the html svg tag. It then sets the height and width of the pane and sets values for how far and fast the nodes and relationships should bounce away from one another (var force). Once that is set, we set our svg variable to the graph html element on our page with some basic styling and attributes.

The next block actually populates our data within the visualization. First, we call d3.json that will go to our /buildgraph endpoint in our application and then call a function. Remember that our /buildgraph endpoint will execute our graph() method to gather all of the data and format it as two maps. If the call errors, it will simply return; otherwise, it will continue.

In the force.nodes().links().start() code, it is pulling the nodes (from graph.nodes) and the relationships (graph.links) and using the force-directed graph approach, which means that the view will center on them and the nodes will bounce away from one another and not sit on top of one another. We set variables for each node and relationship and loop through, returning values and building circles and lines for each one dynamically.

The last couple of blocks shown in this section were for extra features. This creates a tooltip that displays a node’s type (whether a character, event, or other) and the name of the node (creator’s name, comic issue title, or other). We thought this added a bit more helpful information, so users could explore the graph and see which node colors belonged to each category. Of course, we could definitely continue to improve the user experience here, and we will talk more about possibilities and upcoming ventures in our next blog post!

What I Learned

I’m not a skilled frontend developer, so much of this was used from examples I had dealt with in the past and then customized for our application. I can still definitely make improvements, but I enjoyed the learning experience and seeing some interesting and simple things I could do that made a big difference on the page.

  1. Though the user experience is not stunning, this approach blends simple with a few nicer features. It provides a fun way to adjust certain settings and formatting to see how it impacts all of our data and the rendering. The examples I had seen allowed us to build a simple and nice interface for us and other users to interact with the data, search for comics, and navigate the page.
  2. I got more in-depth exposure to D3 and how to render data with html tags and css. Styling has not been my forte, but I’ve always enjoyed learning about it and seeing the tangible changes in coloring or formatting. It also got me thinking about ways to continue improving the experience and interaction with the different panes.

Next Steps

We have now progressed from initial project seed stages through a full-stack application! Congratulations for making it this far on the journey with me. In the next post, we will take a look at some of the ways where this application could be improved, as well as the full list of project resources and thoughts for future projects we tackle.

Stay tuned!

Resources


Create a Data Marvel — Part 9: Building the Webpage for Comics was originally published in neo4j on Medium, where people are continuing the conversation by highlighting and responding to this story.