Building the Hexations banner

A key part of any new blog is customising your landing page. Inspired by the amazing Live Free or Dichotomize blog,https://livefreeordichotomize.com/ maintained by Nick Strayer and Lucy D’Agostino McGowan, I was excited to create my own banner image for NTS.

I had two main criteria for the new banner, in that I wanted it to be:

  • interactive; and
  • unique to each visitor.

In terms of a framework for building the banner, as I had been keen to try and use D3 for a while, this seemed like a good project to do so! As a brief overview, D3, short for Data-Driven Documents, is a JavaScript library for creating compelling data visualisations using HTML, SVG, and CSS. It also provides powerful interaction techniques.

As I had already been using a grey hexagon as the blog’s logo and favicon,Imaginative, I know. . . I wanted the banner to be in keeping with this theme. Fortunately, I found a wonderful tutorial on creating a hexagon mesh grid in D3,https://bl.ocks.org/mbostock/5249328 which I was able to adapt and expand to create my banner.

Getting started

To kick things off, I needed to import the D3.js library. I also needed the topojson.js library to help create the hexagons and the jquery.js to allow me to access the calculated widths of html elements:

<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script
  src="https://code.jquery.com/jquery-3.2.1.min.js"
  integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
  crossorigin="anonymous"></script>

Next, I created a division with the ID “header-viz”:

<div id = "header_viz"></div>

In the D3 code below, I access the calculated width of the “p” (paragraph) element on the homepage using $("p").width(), which draws on the jQuery library, so that (a) the banner is the same width as the text on the landing page (~55%), and (b) the image is correctly sized to fit whatever screen it is on.

Having allowed the width to be calculated automatically, I set the height to be fixed at 200, after playing around with some different options and finding this to be the one that looked best.

Using D3, I then selected the “header-viz” division I had created above, added (or more accurately, append-ed) an SVG element, and then assigned the calculated width and hard-coded height as attributes using the format .attr("ATTRIBUTE","VALUE"):

All D3 code either needs to be wraped in a “script” element, or placed in a seperate "*.js" file and read into the HTML document.

var width   = $("p").width(),
    height  = 200;

var svg = d3.select("#header_viz").append("svg")
.attr("width", width)
.attr("height", height)

Now that I had an empty SVG image, it was time to add the title of the blog. The advantage of creating the height and width as variables, rather than directly assigning them using .attr("height", 200) for example, becomes clear here as it allows these values to be reused to correctly position the text in the centre of the image. The text-anchor="middle" and alignment-baseline="central" attributes are used to center the text around the centre-point coordinates (by default, text is aligned to start at the coordinates provided):

svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("class", "banner-text")
.attr('text-anchor', "middle")
.attr("alignment-baseline", "central")
.text("Hexations")
.style('fill', '#4a4a4a')

The resulting image looked like this:

Add the hexagons

Having defined the title, it was time to add the hexagons. This element drew heavily on the D3 tutorial mentioned in the introduction. I haven’t reproduced all the relevant code here, as there is quite a lot. If you are interested, you can see the how the data (topology.objects.hexagons.geometries) and functions (e.g. topojson.feature) used in this block are defined in the file included at the end of this post.

svg.append("g")
.attr("class", "hexagon")
.selectAll("path")
.data(topology.objects.hexagons.geometries)
.enter().append("path")
.attr("d", function(d) {
  return path(topojson.feature(topology, d))
  
})
.attr("class", function(d) {
  return d.fill ? "nofill":"fill"
  
})

The important thing to note here is that the “class” of each hexagon is being defined by a function that quasi-randomly assigns a value of either “fill” or “nofill”, so that every time a user visits or refreshes the page, they should see a different starting configuration of filled hexagons. I say quasi-randomly, because the function produces a random number for each hexagon, and if the number is lower than some threshold, it assigns a class of “fill”. I defined the threshold to decrease dramatically as you move left-to-right across the hexagon grid in order to create the cluster of filled hexagons on the left-hand side of the image.

In the corresponding CSS, I defined fill colours for the hexagons. The default colour for hexagons with a class of “nofill” was transparent, allowing the background colour to show through. This was necessary as otherwise the “Hexations” text would not be visible. Alternatively, if the hexagon had a class of “fill”, it was coloured a darker grey:

.hexagon {
  fill: transparent;
  pointer - events:all;
}

.hexagon .fill {
  fill:#4A4A4A;
}

So now the image looked like:


Adding reactvity

On hover

In terms of interactivity, the first thing I implemented was for any transparent hexagon to fill on hover - I wanted it to be very obvious when a visitor moved their mouse over the banner that it was interactive. While I could have done this with D3 using .on("mouseover",FUN), a simpler solution was to add a :hover selector to the .hexagon .nofill class in the CSS file:

.hexagon .nofill:hover {
  fill: #4A4A4A;
}

Now, any transparent hexagon will temporarily fill as long as the cursor is hovering over it.

Hover over a hexagon to see the interactivity.


Permanent interactivity

The final thing I wanted to do was to allow visitors to permanently fill in a hexagon by clicking on it.

This involved creating some functions to respond to mouse events, which changed the class of a hexagon from “nofill” to “fill” on click:

svg.append("g")
  .attr("class", "hexagon")
  .selectAll("path")
  .data(topology.objects.hexagons.geometries)
  .enter().append("path")
.attr("d", function(d) {
  return path(topojson.feature(topology, d))
  
})
.attr("class", function(d) {
  return d.fill ? "nofill":"fill"
  
})
  .on("mousedown", mousedown)
  .on("mousemove", mousemove)
  .on("mouseup", mouseup)


  var mousing = 0;
  
  function mousedown(d) {
    mousing = d.fill ? -1 : +1;
    mousemove.apply(this, arguments);
  }
  
  function mousemove(d) {
    if (mousing) {
      d3.select(this)
        .attr("class", "fill")
        .classed("nofill", d.fill = mousing > 0);
        
    }
  }
  
  function mouseup() {
    mousemove.apply(this, arguments);
    mousing = 0;
  }


Click on a single non-filled hexagon, or click and drag, to fill in the banner.


Materials

Everything needed to reproduce the banner is included in the hidden code block below. Copy it into a file and save it as HTML, then open with a web browser to view.

banner.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

  .mesh {
    fill: none;
    stroke: transparent;
    stroke-opacity: .2;
    pointer-events: none;
  }
  
  .border {
    fill: none;
    stroke: transparent;
    stroke-width: 2px;
    pointer-events: none;
  }
  
  .banner-text {
    font-size: 3.2rem;
    font-weight: 400;
  }

  .hexagon path {
    -webkit-transition: fill 250ms linear;
    transition: fill 250ms linear;
  }
  
  .hexagon {
      fill: transparent;
      pointer-events: all;
  }
  
  .hexagon .fill {
    fill: #4A4A4A;
  }
  

  .hexagon .nofill:hover {
    fill: #4A4A4A;
  }
  

</style>
    
<p></p>
<div id = "header_viz"></div>

<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script
  src="https://code.jquery.com/jquery-3.2.1.min.js"
  integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
  crossorigin="anonymous"></script>


<script>

  var width   = $("p").width(),
      height  = 200;
  
  var radius = 20;
  
  var topology = hexTopology(radius, width, height);
  
  var projection = hexProjection(radius);
  
  var path = d3.geo.path()
  .projection(projection);
  
  var svg = d3.select("#header_viz").append("svg")
  .attr("width", width)
  .attr("height", height)

  svg.append("text")
  .attr("x", width/2)
  .attr("y", height/2)
  .attr("class", "banner-text")
  .attr('text-anchor', "middle")
  .attr("alignment-baseline","central")
  .text("Hexations")
  .style('fill', '#4a4a4a')
  
  svg.append("g")
  .attr("class", "hexagon")
  .selectAll("path")
  .data(topology.objects.hexagons.geometries)
  .enter().append("path")
.attr("d", function(d) {
  return path(topojson.feature(topology, d))
  
})
.attr("class", function(d) {
  return d.fill ? "nofill":"fill"
  
})
  .on("mousedown", mousedown)
  .on("mousemove", mousemove)
  .on("mouseup", mouseup)


  var mousing = 0;
  
  function mousedown(d) {
    mousing = d.fill ? -1 : +1;
    mousemove.apply(this, arguments);
  }
  
  function mousemove(d) {
    if (mousing) {
      d3.select(this)
        .attr("class", "fill")
        .classed("nofill", d.fill = mousing > 0);
    }
  }
  
  function mouseup() {
    mousemove.apply(this, arguments);
    mousing = 0;
  }
  

  function hexTopology(radius, width, height) {
    var dx = radius * 2 * Math.sin(Math.PI / 3),
    dy = radius * 1.5,
    m = Math.ceil((height + radius) / dy) + 1,
    n = Math.ceil(width / dx) + 1,
    geometries = [],
    arcs = [];
    
    for (var j = -1; j <= m; ++j) {
      for (var i = -1; i <= n; ++i) {
        var y = j * 2, x = (i + (j & 1) / 2) * 2;
        arcs.push([[x, y - 1], [1, 1]], [[x + 1, y], [0, 1]], [[x + 1, y + 1], [-1, 1]]);
      }
    }
    
    for (var j = 0, q = 3; j < m; ++j, q += 6) {
      for (var i = 0; i < n; ++i, q += 3) {
        geometries.push({
          type: "Polygon",
          arcs: [[q, q + 1, q + 2, ~(q + (n + 2 - (j & 1)) * 3), ~(q - 2), ~(q - (n + 2 + (j & 1)) * 3 + 2)]],
          fill: Math.random() < i / n * 4
        });
      }
    }
    
    return {
      transform: {translate: [0, 0], scale: [1, 1]},
      objects: {hexagons: {type: "GeometryCollection", geometries: geometries}},
      arcs: arcs
    };
  }
  
  function hexProjection(radius) {
    var dx = radius * 2 * Math.sin(Math.PI / 3),
    dy = radius * 1.5;
    return {
      stream: function(stream) {
        return {
          point: function(x, y) { stream.point(x * dx / 2, (y - (2 - (y & 1)) / 3) * dy / 2); },
          lineStart: function() { stream.lineStart(); },
          lineEnd: function() { stream.lineEnd(); },
          polygonStart: function() { stream.polygonStart(); },
          polygonEnd: function() { stream.polygonEnd(); }
        };
      }
    };
  }
  
</script>

Luke A McGuinness image
Luke A McGuinness

An NIHR Doctoral Research Fellow at the University of Bristol, Luke is passionate about dementia epidemiology, open science and R.