Chord Pro Features For Chord Diagrams

Preamble

In [1]:
from chord import Chord

Introduction

Note

This document is best viewed online where the interactivity of the demonstrations can be experienced.

In a chord diagram (or radial network), entities are arranged radially as segments with their relationships visualised by arcs that connect them. The size of the segments illustrates the numerical proportions, whilst the size of the arc illustrates the significance of the relationships1. Chord diagrams are useful when trying to convey relationships between different entities, and they can be beautiful and eye-catching.

Get Chord Pro

Click here to get lifetime access to the full-featured chord visualization API, producing beautiful interactive visualizations, e.g. those featured on the front page of Reddit.

chord pro

License

To switch to the PRO version of the chord package, you need to assign a valid username (the email you entered at purchase) and license key. This can be purchased here.

In [2]:
Chord.user = "your username"
Chord.key = "your license key"

We'll use the following data for the co-occurrence matrix and names parameters until we cover divided diagrams.

In [3]:
matrix = [
    [0, 5, 6, 4, 7, 4],
    [5, 0, 5, 4, 6, 5],
    [6, 5, 0, 4, 5, 5],
    [4, 4, 4, 0, 5, 5],
    [7, 6, 5, 5, 0, 4],
    [4, 5, 5, 5, 4, 0],
]

names = ["Action", "Adventure", "Comedy", "Drama", "Fantasy", "Thriller"]

Defaults

Without passing in any arguments for the customisation parameters, the output will use the default value.

In [4]:
Chord(matrix, names).show()
Chord Diagram

Outputs Methods

Chord Pro supports the following outputs.

HTML to Jupyer Lab Cell (Interactive)

In [5]:
Chord(matrix, names).show()
Chord Diagram

HTML to file (Interactive)

In [6]:
Chord(matrix, names).to_html('out.html')

PNG to Jupyer Lab Cell (Image)

In [7]:
Chord(matrix, names).show_png()

PNG to file (image)

In [8]:
Chord(matrix, names).to_png('out.png')

Chord Colours

colors="d3.schemeSet1"

The default setting for the chord colours is d3.schemeSet1.

This can be changed to any of the sequential, diverging, or categorical colour schemes in d3-scale-chromatic, such as d3.schemeAccent, d3.schemeBlues[n], or d3.schemePaired :

In [9]:
Chord(matrix, names, colors="d3.schemeAccent").show()
Chord Diagram

The colors parameter also accepts a Python list of HEX colour codes.

In [10]:
grayscale = ["#222222", "#333333", "#4c4c4c", "#666666", "#848484", "#9a9a9a"]
Chord(matrix, names, colors=grayscale).show()
Chord Diagram

Opacity

opacity=0.8

This sets the opacity for the arcs when they are not selected (mouseover/touch).

In [11]:
Chord(matrix, names, opacity=0.2).show()
Chord Diagram

Reverse Gradients

opacity=0.8

This sets the direction of the arc gradients.

In [12]:
Chord(matrix, names, reverse_gradients=True).show()
Chord Diagram

Arc Numbers

arc_numbers=False

This sets the visibility of quantity labels on segments.

In [13]:
Chord(matrix, names, arc_numbers=True).show()
Chord Diagram

Diagram Title

titles=""

This sets the text and visibility of the diagram title.

In [14]:
Chord(matrix, names, title="Movie Genre Co-occurrence").show()
Chord Diagram

Padding

padding=0.01

This sets the padding between segments as a fraction of the circle.

In [15]:
Chord(matrix, names, padding=0.5).show()
Chord Diagram

Width

width=700

This sets the width (and height) of the chord diagram.

In [16]:
Chord(matrix, names, width=400).show()
Chord Diagram

Rotation

rotate=0

This sets the rotation of the chord diagram.

In [17]:
Chord(matrix, names, rotate=-30).show()
Chord Diagram

Radius Scales

inner_radius_scale=0.45, outer_radius_scale=1.1,

This sets the inner and outer radius scale of the chord diagram.

In [18]:
Chord(matrix, names, inner_radius_scale=0.3, outer_radius_scale=1.5).show()
Chord Diagram

Label Color

label_color="#454545"

This sets the label colour.

In [19]:
Chord(matrix, names, label_color="#B362FF").show()
Chord Diagram

Curved Labels

curved_labels=False

This turns curved labels on or off. It's not suitable for diagrams with many segments, but sometimes it can improve the look.

In [20]:
Chord(matrix, names, curved_labels=True).show()
Chord Diagram

Label Wrapping

wrap_labels=True

This turns label wrapping on or off. It's on by default.

Let's temporarily change our names data to demonstrate this.

In [21]:
names = ["Exciting Action", "Fun Adventure", "Hilarious Comedy", "Drama", "Fantasy", "Chilling Thriller"]
In [22]:
Chord(matrix, names).show()
Chord Diagram

Now to demonstrate label wrapping set to disabled.

In [23]:
Chord(matrix, names, wrap_labels=False).show()
Chord Diagram

We'll restore our names data before we continue.

In [24]:
names = ["Action", "Adventure", "Comedy", "Drama", "Fantasy", "Thriller"]

Margins

margin=100

This sets the chord diagram margin.

In [25]:
Chord(matrix, names, margin=200).show()
Chord Diagram

Font-size

font_size="16px" font_size_large="20px"

This sets the font-size for two views, font-size-large (@media min-width: 600px), otherwise font-size. Setting a large font-size may require adjustment of the margin parameter to avoid text-clipping.

In [26]:
Chord(matrix, names, font_size="20px", font_size_large="30px").show()
Chord Diagram

popup_width=350

This sets the max-width of the popup.

In [27]:
Chord(matrix, names, popup_width=150).show()
Chord Diagram

conjunction="and" verb="occur together in" noun="instances"

These change parts of the popup text:

Default popup text

e.g. we could change it to:

Modified popup text

In [28]:
Chord(matrix, names, conjunction="&", verb="appear together in", noun="cases",).show()
Chord Diagram

Symmetric and Asymmetric Diagrams

symmetric=True

This turns the symmetric mode on or off. The primary support is for symmetric diagrams, but there is some support for asymmetric ones.

For example, this will update the popup text to reflect the asymmetry of relationships.

Asymmetric mode popup text

In [29]:
Chord(matrix, names, symmetric=False).show()
Chord Diagram

Download Button

allow_download=False

This sets the visibility of the Download button. This uses client-side scripts to create and download an SVG from the current visualisation. This has been tested on Firefox and Safari.

In [30]:
Chord(matrix, names, allow_download=True).show()
Chord Diagram
Download

details=[], details_thumbs=[],

In the basic version of chord, matrix and names are the only sets of data that can be used to create a chord diagram. In the Pro version, you can also use details and details_thumbs. These enable the rich hover boxes.

Rich popup

First, we'll reduce the co-occurrence frequency in our matrix to make the following examples easier to follow.

In [31]:
matrix = [
    [0, 2, 3, 1, 4, 1],
    [2, 0, 2, 1, 3, 2],
    [3, 2, 0, 1, 2, 2],
    [1, 1, 1, 0, 2, 2],
    [4, 3, 2, 2, 0, 1],
    [1, 2, 2, 2, 1, 0],
]

Let's create and populate an example details and details_thumbs variable.

In [32]:
details = [
    [[], ["Movie 1","Movie 2"], ["Movie 3","Movie 4","Movie 5"], ["Movie 6","Movie 7"], ["Movie 8","Movie 9","Movie 10","Movie 11"], ["Movie 12"]],
    [["Movie 13","Movie 14"], [], ["Movie 15","Movie 16"], ["Movie 17"], ["Movie 18","Movie 19","Movie 20"], ["Movie 21","Movie 22"]],
    [["Movie 23","Movie 24","Movie 25"], ["Movie 26","Movie 27"], [], ["Movie 28"], ["Movie 29","Movie 30"], ["Movie 31","Movie 32"]],
    [["Movie 33"], ["Movie 34"], ["Movie 35"], [], ["Movie 36","Movie 37"], ["Movie 38","Movie 39"]],
    [["Movie 40","Movie 41","Movie 42","Movie 43"], ["Movie 44","Movie 45","Movie 46"], ["Movie 47","Movie 48"], ["Movie 49","Movie 50"], [], ["Movie 51"]],
    [["Movie 52"], ["Movie 53","Movie 54"], ["Movie 55","Movie 56"], ["Movie 57","Movie 58"], ["Movie 59"], []],
]

The details parameter can accept HTML.

In [33]:
details_thumbs = [
    [[], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png"]],
    [["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], [], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"]],
    [["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], [], ["https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"]],
    [["https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png"], [], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"]],
    [["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], [], ["https://datacrayon.com/images/lablet.png"]],
    [["https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png","https://datacrayon.com/images/lablet.png"], ["https://datacrayon.com/images/lablet.png"], []],
]

It looks a bit messy, so let's achieve some clarity using the pandas.DataFrame HTML table output.

In [34]:
import pandas as pd
pd.DataFrame(details)
Out[34]:
0 1 2 3 4 5
0 [] [Movie 1, Movie 2] [Movie 3, Movie 4, Movie 5] [Movie 6, Movie 7] [Movie 8, Movie 9, Movie 10, Movie 11] [Movie 12]
1 [Movie 13, Movie 14] [] [Movie 15, Movie 16] [Movie 17] [Movie 18, Movie 19, Movie 20] [Movie 21, Movie 22]
2 [Movie 23, Movie 24, Movie 25] [Movie 26, Movie 27] [] [Movie 28] [Movie 29, Movie 30] [Movie 31, Movie 32]
3 [Movie 33] [Movie 34] [Movie 35] [] [Movie 36, Movie 37] [Movie 38, Movie 39]
4 [Movie 40, Movie 41, Movie 42, Movie 43] [Movie 44, Movie 45, Movie 46] [Movie 47, Movie 48] [Movie 49, Movie 50] [] [Movie 51]
5 [Movie 52] [Movie 53, Movie 54] [Movie 55, Movie 56] [Movie 57, Movie 58] [Movie 59] []
In [35]:
pd.DataFrame(details_thumbs)
Out[35]:
0 1 2 3 4 5
0 [] [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png]
1 [https://datacrayon.com/images/lablet.png, htt... [] [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png] [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt...
2 [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [] [https://datacrayon.com/images/lablet.png] [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt...
3 [https://datacrayon.com/images/lablet.png] [https://datacrayon.com/images/lablet.png] [https://datacrayon.com/images/lablet.png] [] [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt...
4 [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [] [https://datacrayon.com/images/lablet.png]
5 [https://datacrayon.com/images/lablet.png] [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png, htt... [https://datacrayon.com/images/lablet.png] []
In [36]:
Chord(matrix, names, details=details, details_thumbs=details_thumbs).show()
Chord Diagram

thumbs_width=85 thumbs_margin=5 thumbs_font_size=14

These allow changing the spacing and size of the details thumbnails.

In [37]:
Chord(matrix, names, details=details, details_thumbs=details_thumbs,
      thumbs_width=40, thumbs_margin=10, thumbs_font_size=10).show()
Chord Diagram

Divided (Bipartite) Chord Diagram

divide=False, divide_idx=0, divide_size=0.5,

  • divide enables/disables the divided chord diagram mode.
  • divide_idx sets the dividing point of the matrix.
  • divide_size sets the size of the spacing that creates the divide.

First, we'll change the data in matrix and names such that they're suitable for our bipartite chord diagram. We'll add some new colours too.

In [38]:
matrix = [
    [0, 0, 0, 1, 4, 1],
    [0, 0, 0, 1, 3, 2],
    [0, 0, 0, 1, 2, 2],
    [1, 1, 1, 0, 0, 0],
    [4, 3, 2, 0, 0, 0],
    [1, 2, 2, 0, 0, 0],
]

names = ["A", "B", "C", "1", "2", "3"]
colors = ["#7400B8", "#5E60CE", "#5684D6", "#56CFE1", "#64DFDF", "#80FFDB"]
In [39]:
Chord(matrix, names, colors=colors, divide=True, divide_idx=3).show()
Chord Diagram

Divided (Bipartite) Chord Diagram Labels

divide_left_label="", divide_right_label=""

These parameters allow you to label either side of the divided chord diagram.

In [40]:
Chord(matrix, names, colors=colors, divide=True, divide_idx=3,
     divide_left_label="Left Side", divide_right_label="Right Side").show()
Chord Diagram

Mix and Match

In [41]:
matrix = [
    [0, 5, 6, 4, 7, 4],
    [5, 0, 5, 4, 6, 5],
    [6, 5, 0, 4, 5, 5],
    [4, 4, 4, 0, 5, 5],
    [7, 6, 5, 5, 0, 4],
    [4, 5, 5, 5, 4, 0],
]

names = ["Action", "Adventure", "Comedy", "Drama", "Fantasy", "Thriller"]
In [42]:
Chord(matrix, names, padding=0.5, reverse_gradients=True, colors="d3.schemeAccent",
     curved_labels=True, label_color="#777777", font_size_large="30px",
      inner_radius_scale=0.45, outer_radius_scale=1.4, arc_numbers=True).show()
Chord Diagram

  1. Tintarev, N., Rostami, S., & Smyth, B. (2018, April). Knowing the unknown: visualising consumption blind-spots in recommender systems. In Proceedings of the 33rd Annual ACM Symposium on Applied Computing (pp. 1396-1399). 

Dreamcast Intro (Preview)

Colour Transitions

Highlight

Whilst we could use the string literal magenta as our arguement to change the fill colour, we'll use its hexadecimal equivalent, #ff00ff instead. This will give us more opportunity should we want to tinker with the colours. We could also use an RGB value, e.g. rgb(255, 0, 255).

Preamble

Let's get access to the D3.js library so that we can begin. In this case, we'll be including the library using the HTML <script> tag.

<script src="https://d3js.org/d3.v6.js"></script>

Introduction

The last few sections on animated transitions have focussed manipulating the geometric properties of various SVG shape elements1. In this section, we'll look at how we can use transitions and styling properties to animate the colour of SVG shape elements.

Let's use D3js to transition a circle starting from the colour magenta.

Which will then gradually turn to the colour cyan.

We'll also make use of an easing function to make the transitions more interesting. These two transitions will occur one after another without any condition for termination, i.e. forever!

A Container for the Output

This is where you will see the output of the code cells that follow it, provided they are referencing the corresponding id.

<div id="container"></div>

Creating an Empty SVG

We'll create a new detached <svg> element and use the returned selection throughout the rest of this section.

const svg = d3.create("svg");

Creating a Circle Element

Let's create our circle! We'll append the <circle> element to our selection of the <svg> element, and we'll give it a fill colour of magenta using the style attribute, .style("fill", "magenta");. Whilst we could use the string literal magenta as our argument to change the fill colour, we'll use its hexadecimal equivalent, #ff00ff instead. This will give us more opportunity should we want to tinker with the colours. We could also use an RGB value, e.g. rgb(255, 0, 255).

var circle = svg
    .append("circle")
    .attr("cx", 150)
    .attr("cy", 75)
    .attr("r", 50)
    .style("fill", "#ff00ff");

Animating the Circle Colour with Easing

Let's create two functions, one to colour our circle magenta, and another to colour it cyan. This will be similar to previous sections where we've expanded a circle with something much like the following:

function expandCircle() {
    circle
        .transition()
        .ease(d3.easePoly)
        .duration(1000)
        .attr('r', 75)
        .on('end', contractCircle);
}

Except, this time we won't be changing the geometric properties of our circle.

Function to Colour the Circle Magenta

We'll start with the function to colour the circle magenta, which we'll name colourMagenta().

function colourMagenta() {
    circle
        .transition()
        .ease(d3.easePoly)
        .duration(2000)
        .style("fill", "#ff00ff")
        .on('end', colourCyan);
}

When this transition ends, it will call the colourCyan() function which doesn't exist just yet.

Function to Colour the Circle Cyan

Let's create our missing function to colour the circle cyan, which we'll name colourCyan().

function colourCyan() {
    circle
        .transition()
        .ease(d3.easeBounce)
        .duration(2000)
        .style("fill", "#00ffff")
        .on('end', colourMagenta);
}

This time, we can see we've added a listener function to call colourMagenta() when the transition ends.

Starting the Animation

The code that handles our transitions now appears within two functions. That means that nothing will happen on page load unless we invoke one of these functions directly to kick everything off. Let's start our looping transitions by invoking colourMagenta(), which will call colourCyan() when it ends, and so on!

colourMagenta();

Appending to the Container

Finally, let's append everything to our container.

d3
    .select("#container")
    .append(() => svg.node());

We can see the output by checking on our container with the corresponding id, which in this case is where id=container.

Conclusion

If we inspect the HTML, we will see the <svg> and <circle> elements have been added to the <div> where the id=container. We can also see that the <circle> element's style attribute includes a fill that is gradually changing between magenta (style="fill: rgb(255, 0, 255)") and cyan (style="fill: rgb(0, 255, 255)").

<div id="container">
  <svg>
    <circle cx="150" cy="75" r="50" style="fill: rgb(255, 0, 255);"></circle>
  </svg>
</div>

  1. W3C. Geometry Properties, https://www.w3.org/TR/SVG2/geometry.html. 

Animated Transitions with Easing

Highlight

D3.js transitions support easing functions which can change the speed at which an animation progresses throughout its duration. There are many different easing functions available in D3.js. Examples, descriptions, and visualisations of each one can be found in the API reference.

Preamble

Let's get access to the D3.js library so that we can begin. In this case, we'll be including the library using the HTML <script> tag.

<script src="https://d3js.org/d3.v6.js"></script>

Introduction

D3.js transitions support easing functions which can change the speed at which an animation progresses throughout its duration1. For example, we may wish to play an animation that moves quickly at the start, and then moves slowly towards the end:

This is different from the default behaviour which is cubic:

Let's modify an earlier example which aims to visualise a circle shape that starts with a radius of $50$.

Which will then gradually expand to a radius of $75$.

Except, this time we'll give it the appearance of bouncing back down to its original radius. These two transitions will occur one after another without any condition for termination, i.e. forever!

A Container for the Output

This is where you will see the output of the code cells that follow it, provided they are referencing the corresponding id.

<div id="container"></div>

Creating an Empty SVG

We'll create a new detached <svg> element and use the returned selection throughout the rest of this section.

const svg = d3.create("svg");

Creating a Circle Element

Let's create our circle! We'll append the <circle> element to our selection of the <svg> element, and we'll use our starting coordinates ${150,75}$ and radius of ${50}$.

var circle = svg
    .append("circle")
    .attr("cx", 150)
    .attr("cy", 75)
    .attr("r", 50);

Animating the Circle Element with Easing

Let's create two functions, one to expand our circle, and another to shrink it. This will be similar to previous sections where we've expanded a circle with something much like the following:

function expandCircle() {
    circle
        .transition()
        .duration(1000)
        .attr('r', 75)
        .on('end', contractCircle);
}

Except, this time we will introduce a minor modification to our expandCircle() and contractCircle() functions, where we apply an easing function to control the rate of change. In this case, let's use the d3.easePoly and d3.easeBounce easing functions within expandCircle() and contractCircle(), respectively.

There are many different easing functions available in D3.js. Examples, descriptions, and visualisations of each one can be found in the API reference1.

Function to Expand the Circle

We'll start with the function to expand the circle, which we'll name expandCircle().

function expandCircle() {
    circle
        .transition()
        .ease(d3.easePoly)
        .duration(1000)
        .attr('r', 75)
        .on('end', contractCircle);
}

When this transition ends, it will call the contractCircle() function which doesn't exist just yet.

Function to Contract the Circle

Let's create our missing function to contract the circle, which we'll name contractCircle().

function contractCircle() {
    circle
        .transition()
        .ease(d3.easeBounce)
        .duration(1000)
        .attr('r', 50)
        .on('end', expandCircle);
}

This time, we can see we've added a listener function to call expandCircle() when the transition ends.

Starting the Animation

The code that handles our transitions now appears within two functions. That means that nothing will happen on page load unless we invoke one of these functions directly to kick everything off. Let's start our looping transitions by invoking expandCircle(), which will call contractCircle() when it ends, and so on!

expandCircle();

Appending to the Container

Finally, let's append everything to our container.

d3
    .select("#container")
    .append(() => svg.node());

We can see the output by checking on our container with the corresponding id, which in this case is where id=container.

Conclusion

If we inspect the HTML, we will see the <svg> and <circle> elements have been added to the <div> where the id=container. We can also see that the <circle> element's r attribute is gradually increasing to $75$ and then bouncing back down to $50$ repeatidly, giving the appearance of a gradually expanding circle that pops and bounces back down to its starting radius.

<div id="container">
  <svg>
    <circle cx="150" cy="75" r="75"></circle>
  </svg>
</div>

  1. M. Bostock. d3-ease: Easing functions for smooth animation https://github.com/d3/d3-ease. 

  2. M. Bostock. d3-ease: API Reference https://github.com/d3/d3-ease#api-reference. 

Looping Animated Transitions

Highlight

We've introduced this new invocation of the transition.on(typenames, listener) function. This can add a listener function to the selection, which is invoked based on the event type. We've used an event type of end because we want our transition to end before calling the next one.

Preamble

Let's get access to the D3.js library so that we can begin. In this case, we'll be including the library using the HTML <script> tag.

<script src="https://d3js.org/d3.v6.js"></script>

Introduction

D3.js transitions support control flow for advanced behaviour. For example, we may wish to play one transition after another and repeat them indefinitely to create the appearance of a looping animation.

As an example, we'll aim to visualise a circle shape that starts with a radius of $50$.

Which will then gradually expand to a radius of $75$.

Before shrinking back down to its original radius. These two transitions will occur one after another without any condition for termination, i.e. forever!

A Container for the Output

This is where you will see the output of the code cells that follow it, provided they are referencing the corresponding id.

<div id="container"></div>

Creating an Empty SVG

We'll create a new detached <svg> element and use the returned selection throughout the rest of this section.

const svg = d3.create("svg");

Creating a Circle Element

Let's create our circle! We'll append the <circle> element to our selection of the <svg> element, and we'll use our starting coordinates ${150,75}$ and radius of ${50}$.

var circle = svg
    .append("circle")
    .attr("cx", 150)
    .attr("cy", 75)
    .attr("r", 50);

Animating the Circle Element on a Loop

Let's create two functions, one to expand our circle, and another to shrink it. This will be similar to previous sections where we've expanded a circle with something much like the following:

circle
    .transition()
    .duration(2000)
    .attr('r', 75);

Except, this time we will wrap the code in a function so that it can be called multiple times, and also introduce a listener that we will use for control flow.

Function to Expand the Circle

We'll start with the function to expand the circle, which we'll name expandCircle().

function expandCircle() {
    circle
        .transition()
        .duration(1000)
        .attr('r', 75)
        .on('end', contractCircle);
}

Besides wrapping the code in a function, we've introduced this new invocation of the transition.on(typenames, listener) function. This can add a listener function to the selection, which is invoked based on the event type. We've used an event type of end because we want our transition to end before calling the next one, but the following are also available1:

  • start - when the transition starts.
  • end - when the transition ends.
  • interrupt - when the transition is interrupted.
  • cancel - when the transition is cancelled.

When this transition ends, it will call the contractCircle() function which doesn't exist just yet.

Function to Contract the Circle

Let's create our missing function to contract the circle, which we'll name contractCircle().

function contractCircle() {
    circle
        .transition()
        .duration(1000)
        .attr('r', 50)
        .on('end', expandCircle);
}

This time, we can see we've added a listener function to call expandCircle() when the transition ends.

Starting the Animation

The code that handles our transitions now appears within two functions. That means that nothing will happen on page load unless we invoke one of these functions directly to kick everything off. Let's start our looping transitions by invoking expandCircle(), which will call contractCircle() when it ends, and so on!

expandCircle();

Appending to the Container

Finally, let's append everything to our container.

d3
    .select("#container")
    .append(() => svg.node());

We can see the output by checking on our container with the corresponding id, which in this case is where id=container.

Conclusion

If we inspect the HTML, we will see the <svg> and <circle> elements have been added to the <div> where the id=container. We can also see that the <circle> element's r attribute is gradually increasing to $75$ and then back down to $50$ repeatidly, giving the appearance of an expanding and contracting circle.

<div id="container">
  <svg>
    <circle cx="150" cy="75" r="75"></circle>
  </svg>
</div>

  1. M. Bostock. d3-transition: Control Flow https://github.com/d3/d3-transition#transition_on. 

Arabica Coffee Beans - Origin and Variety

Preamble

In [1]:
import itertools
import pandas as pd  # for DataFrames
from chord import Chord

Introduction

In this section, we're going to be pointing our beautifully colourful lens towards the warm and aromatic world of coffee. In particular, we're going to be visualising the co-occurrence of coffee bean variety and origin in over a thousand coffee reviews.

Note

This section uses the Chord Pro software to create a visualisation. Grab a copy to produce the same output!

The Dataset

We're going to use the popular Coffee Quality Institute Database which I have forked on GitHub for posterity. The file arabatica_data.csv contains the data we'll be using throughout this section, and the first thing we'll want to do is to load the data and output some samples for a sanity check.

In [2]:
data_url = "https://datacrayon.com/datasets/arabica_data.csv"
data = pd.read_csv(data_url)
data.head()
Out[2]:
Unnamed: 0 Species Owner Country.of.Origin Farm.Name Lot.Number Mill ICO.Number Company Altitude ... Color Category.Two.Defects Expiration Certification.Body Certification.Address Certification.Contact unit_of_measurement altitude_low_meters altitude_high_meters altitude_mean_meters
0 1 Arabica metad plc Ethiopia metad plc NaN metad plc 2014/2015 metad agricultural developmet plc 1950-2200 ... Green 0 April 3rd, 2016 METAD Agricultural Development plc 309fcf77415a3661ae83e027f7e5f05dad786e44 19fef5a731de2db57d16da10287413f5f99bc2dd m 1950.0 2200.0 2075.0
1 2 Arabica metad plc Ethiopia metad plc NaN metad plc 2014/2015 metad agricultural developmet plc 1950-2200 ... Green 1 April 3rd, 2016 METAD Agricultural Development plc 309fcf77415a3661ae83e027f7e5f05dad786e44 19fef5a731de2db57d16da10287413f5f99bc2dd m 1950.0 2200.0 2075.0
2 3 Arabica grounds for health admin Guatemala san marcos barrancas "san cristobal cuch NaN NaN NaN NaN 1600 - 1800 m ... NaN 0 May 31st, 2011 Specialty Coffee Association 36d0d00a3724338ba7937c52a378d085f2172daa 0878a7d4b9d35ddbf0fe2ce69a2062cceb45a660 m 1600.0 1800.0 1700.0
3 4 Arabica yidnekachew dabessa Ethiopia yidnekachew dabessa coffee plantation NaN wolensu NaN yidnekachew debessa coffee plantation 1800-2200 ... Green 2 March 25th, 2016 METAD Agricultural Development plc 309fcf77415a3661ae83e027f7e5f05dad786e44 19fef5a731de2db57d16da10287413f5f99bc2dd m 1800.0 2200.0 2000.0
4 5 Arabica metad plc Ethiopia metad plc NaN metad plc 2014/2015 metad agricultural developmet plc 1950-2200 ... Green 2 April 3rd, 2016 METAD Agricultural Development plc 309fcf77415a3661ae83e027f7e5f05dad786e44 19fef5a731de2db57d16da10287413f5f99bc2dd m 1950.0 2200.0 2075.0

5 rows × 44 columns

Data Wrangling

By viewing the CSV directly we can see our desired columns are named Country.of.Origin and Variety. Let's print out the columns to make sure they exist in the data we've loaded.

In [3]:
data.columns
Out[3]:
Index(['Unnamed: 0', 'Species', 'Owner', 'Country.of.Origin', 'Farm.Name',
       'Lot.Number', 'Mill', 'ICO.Number', 'Company', 'Altitude', 'Region',
       'Producer', 'Number.of.Bags', 'Bag.Weight', 'In.Country.Partner',
       'Harvest.Year', 'Grading.Date', 'Owner.1', 'Variety',
       'Processing.Method', 'Aroma', 'Flavor', 'Aftertaste', 'Acidity', 'Body',
       'Balance', 'Uniformity', 'Clean.Cup', 'Sweetness', 'Cupper.Points',
       'Total.Cup.Points', 'Moisture', 'Category.One.Defects', 'Quakers',
       'Color', 'Category.Two.Defects', 'Expiration', 'Certification.Body',
       'Certification.Address', 'Certification.Contact', 'unit_of_measurement',
       'altitude_low_meters', 'altitude_high_meters', 'altitude_mean_meters'],
      dtype='object')

Great! We can see both of these columns exist in our DataFrame.

Now let's take a peek at the unique values in both of these columns to see if any obvious issues stand out. We'll start with the Country.of.Origin column.

In [4]:
data["Country.of.Origin"].unique()
Out[4]:
array(['Ethiopia', 'Guatemala', 'Brazil', 'Peru', 'United States',
       'United States (Hawaii)', 'Indonesia', 'China', 'Costa Rica',
       'Mexico', 'Uganda', 'Honduras', 'Taiwan', 'Nicaragua',
       'Tanzania, United Republic Of', 'Kenya', 'Thailand', 'Colombia',
       'Panama', 'Papua New Guinea', 'El Salvador', 'Japan', 'Ecuador',
       'United States (Puerto Rico)', 'Haiti', 'Burundi', 'Vietnam',
       'Philippines', 'Rwanda', 'Malawi', 'Laos', 'Zambia', 'Myanmar',
       'Mauritius', 'Cote d?Ivoire', nan, 'India'], dtype=object)

We can see some points that may cause issues when it comes to our visualisation.

There appears to be at least one nan value in Country.of.Origin. We're only interested in coffee bean reviews which aren't missing this data, so let's remove any samples where nan exists.

In [5]:
data = data[data["Country.of.Origin"].notna()]

Also, the entries in Country.of.Origin will be used as labels on our visualisation. Ideally, we don't want these to be longer than they need to be. So let's shorten some of the longer names.

In [6]:
data["Country.of.Origin"] = data["Country.of.Origin"].replace(
    "United States (Hawaii)", "Hawaii"
)
data["Country.of.Origin"] = data["Country.of.Origin"].replace(
    "Tanzania, United Republic Of", "Tanzania"
)
data["Country.of.Origin"] = data["Country.of.Origin"].replace(
    "United States (Puerto Rico)", "Puerto Rico"
)

Now let's take a peek at the unique Variety column.

In [7]:
data["Variety"].unique()
Out[7]:
array([nan, 'Other', 'Bourbon', 'Catimor', 'Ethiopian Yirgacheffe',
       'Caturra', 'SL14', 'Sumatra', 'SL34', 'Hawaiian Kona',
       'Yellow Bourbon', 'SL28', 'Gesha', 'Catuai', 'Pacamara', 'Typica',
       'Sumatra Lintong', 'Mundo Novo', 'Java', 'Peaberry', 'Pacas',
       'Mandheling', 'Ruiru 11', 'Arusha', 'Ethiopian Heirlooms',
       'Moka Peaberry', 'Sulawesi', 'Blue Mountain', 'Marigojipe',
       'Pache Comun'], dtype=object)

We can see this column also has at least one nan entry, so let's remove these too.

In [8]:
data = data[data["Variety"].notna()]

Also, there appears to be at least one entry of Other for the Variety. For this visualisation, we're not interested in Other, so let's remove them too.

In [9]:
data = data[data["Variety"] != "Other"]

From previous Chord diagram visualisations we know that they can become too crowded with too many different categories. With this in mind, let's choose to visualise only the top $12$ most frequently occurring Country.of.Origin and Variety.

In [10]:
data = data[
    data["Country.of.Origin"].isin(
        list(data["Country.of.Origin"].value_counts()[:12].index)
    )
]
data = data[data["Variety"].isin(list(data["Variety"].value_counts()[:12].index))]

As we're creating a bipartite chord diagram, let's define what labels will be going on the left and the right.

On the left, we'll have all of our countries of origin.

In [11]:
left = list(data["Country.of.Origin"].value_counts().index)[::-1]
pd.DataFrame(left)
Out[11]:
0
0 China
1 El Salvador
2 Uganda
3 Kenya
4 Hawaii
5 Costa Rica
6 Honduras
7 Taiwan
8 Brazil
9 Colombia
10 Guatemala
11 Mexico

And on the right, we'll have all of our varieties.

In [12]:
right = list(data["Variety"].value_counts().index)
pd.DataFrame(right)
Out[12]:
0
0 Caturra
1 Bourbon
2 Typica
3 Catuai
4 Hawaiian Kona
5 Yellow Bourbon
6 Mundo Novo
7 SL14
8 SL28
9 Pacas
10 Catimor
11 SL34

We're good to go! So let's select just these two columns and work with a DataFrame containing only them as we move forward.

In [13]:
origin_variety = pd.DataFrame(data[["Country.of.Origin", "Variety"]].values)
origin_variety
Out[13]:
0 1
0 Guatemala Bourbon
1 China Catimor
2 Costa Rica Caturra
3 Brazil Bourbon
4 Uganda SL14
... ... ...
884 Honduras Catuai
885 Honduras Catuai
886 Mexico Bourbon
887 Guatemala Catuai
888 Honduras Caturra

889 rows × 2 columns

Our chord diagram will need two inputs: the co-occurrence matrix, and a list of names to label the segments.

We can build this list of names by adding together the labels for the left and right side of our bipartite diagram.

In [14]:
names = left + right
pd.DataFrame(names)
Out[14]:
0
0 China
1 El Salvador
2 Uganda
3 Kenya
4 Hawaii
5 Costa Rica
6 Honduras
7 Taiwan
8 Brazil
9 Colombia
10 Guatemala
11 Mexico
12 Caturra
13 Bourbon
14 Typica
15 Catuai
16 Hawaiian Kona
17 Yellow Bourbon
18 Mundo Novo
19 SL14
20 SL28
21 Pacas
22 Catimor
23 SL34

Now we can create our empty co-occurrence matrix using these type names for the row and column indeces.

In [15]:
matrix = pd.DataFrame(0, index=names, columns=names)
matrix
Out[15]:
China El Salvador Uganda Kenya Hawaii Costa Rica Honduras Taiwan Brazil Colombia ... Typica Catuai Hawaiian Kona Yellow Bourbon Mundo Novo SL14 SL28 Pacas Catimor SL34
China 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
El Salvador 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Uganda 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Kenya 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Hawaii 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Costa Rica 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Honduras 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Taiwan 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Brazil 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Colombia 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Guatemala 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Mexico 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Caturra 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Bourbon 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Typica 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Catuai 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Hawaiian Kona 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Yellow Bourbon 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Mundo Novo 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
SL14 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
SL28 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Pacas 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
Catimor 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
SL34 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

24 rows × 24 columns

We can populate a co-occurrence matrix with the following approach. We'll start by creating a list with every type pairing in its original and reversed form.

In [16]:
origin_variety = list(
    itertools.chain.from_iterable((i, i[::-1]) for i in origin_variety.values)
)

Which we can now use to create the matrix.

In [17]:
for pairing in origin_variety:
    matrix.at[pairing[0], pairing[1]] += 1

matrix = matrix.values.tolist()

We can list the DataFrame for better presentation.

In [18]:
pd.DataFrame(matrix)
Out[18]:
0 1 2 3 4 5 6 7 8 9 ... 14 15 16 17 18 19 20 21 22 23
0 0 0 0 0 0 0 0 0 0 0 ... 2 0 0 0 0 0 0 0 12 0
1 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 2 0 0
2 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 17 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 14 0 0 8
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 44 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0 0 0 0 ... 0 15 0 0 0 0 0 0 0 0
6 0 0 0 0 0 0 0 0 0 0 ... 0 21 0 0 0 0 0 4 1 0
7 0 0 0 0 0 0 0 0 0 0 ... 59 0 0 3 0 0 0 0 0 0
8 0 0 0 0 0 0 0 0 0 0 ... 0 19 0 32 20 0 0 0 0 0
9 0 0 0 0 0 0 0 0 0 0 ... 3 0 0 0 0 0 0 0 0 0
10 0 0 0 0 0 0 0 0 0 0 ... 0 9 0 0 0 0 0 6 0 0
11 0 0 0 0 0 0 0 0 0 0 ... 137 4 0 0 12 0 0 1 0 0
12 0 0 0 0 0 28 23 2 0 129 ... 0 0 0 0 0 0 0 0 0 0
13 0 13 3 0 0 1 0 2 41 0 ... 0 0 0 0 0 0 0 0 0 0
14 2 0 0 0 0 0 0 59 0 3 ... 0 0 0 0 0 0 0 0 0 0
15 0 0 0 0 0 15 21 0 19 0 ... 0 0 0 0 0 0 0 0 0 0
16 0 0 0 0 44 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
17 0 0 0 0 0 0 0 3 32 0 ... 0 0 0 0 0 0 0 0 0 0
18 0 0 0 0 0 0 0 0 20 0 ... 0 0 0 0 0 0 0 0 0 0
19 0 0 17 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
20 0 0 0 14 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
21 0 2 0 0 0 0 4 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
22 12 0 0 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
23 0 0 0 8 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

24 rows × 24 columns

Chord Diagram

Time to visualise the co-occurrence of items using a chord diagram. We are going to use a list of custom colours that represent the items.

Let's specify some colours for the left and right sides.

In [19]:
colors = [
    "#ff575c","#ff914d","#ffca38","#f2fa00","#C3F500","#94f000",
    "#00fa68","#00C1A2","#0087db","#0054f0","#5d00e0","#2F06EB",
    "#6f1d1b","#955939","#A87748","#bb9457","#7f5e38","#432818",
    "#6e4021","#99582a","#cc9f69","#755939","#BAA070","#ffe6a7",]

Finally, we can put it all together using Chord Pro. First, we enter our Chord Pro license details.

In [20]:
Chord.user = "email here"
Chord.key = "license key here"

And then we invoke the Chord function passing in our desired customisation arguments.

In [21]:
Chord(
    matrix,
    names,
    colors=colors,
    wrap_labels=False,
    width=910,
    margin=40,
    padding=0.05,
    font_size="12px",
    font_size_large="18px",
    noun="coffee bean reviews",
    title="Coffee Bean Reviews - Variety and Origin",
    divide=True,
    divide_idx=len(left),
    divide_size=0.6,
    allow_download=True,
).show()
Chord Diagram
Download

Conclusion

In this section, we demonstrated how to conduct some data wrangling on a downloaded dataset to prepare it for a bipartite chord diagram. Our chord diagram is interactive, so you can use your mouse or touchscreen to investigate the co-occurrences!

Animated Transitions

Highlight

Much like d3.selection(), d3.transition() can be used to modify attributes and styles. The difference is that whilst d3.selection() applies the changes instantly, d3.transition() applies the changes gradually (and smoothly) over a specified duration.

Preamble

Let's get access to the D3.js library so that we can begin. In this case, we'll be including the library using the HTML <script> tag.

<script src="https://d3js.org/d3.v6.js"></script>

Introduction

We've already had a look at how to draw SVG shapes with D3.js, but we may often find ourselves wanting to animate these shapes to bring them to life! We can achieve animation using transitions in D3.js1, which enables key-frame animations consisting of two key-frames: start and end. Let's demonstrate a transition by gradually increasing the radius of the below circle, making it appear as if it's growing.

The above circle is positioned at ${150,75}$ with a radius of $50$. Our transition will gradually increase the radius until it looks like the following circle.

By the end of the transition, the circle will have a radius of $75$.

A Container for the Output

This is where you will see the output of the code cells that follow it, provided they are referencing the corresponding id.

<div id="container"></div>

Note

The animation started on page load and lasted two seconds. If you missed it - you can refresh the page to see it in action again!

Creating an Empty SVG

We'll create a new detached <svg> element and use the returned selection throughout the rest of this section.

const svg = d3.create("svg");

Creating a Circle Element

Let's create our circle! We'll append the <circle> element to our selection of the <svg> element, and we'll use our starting coordinates ${150,75}$ and radius of ${50}$.

var circle = svg
    .append("circle")
    .attr("cx", 150)
    .attr("cy", 75)
    .attr("r", 50);

Animating the Circle Element

Much like d3.selection(), d3.transition() can be used to modify attributes and styles. The difference is that whilst d3.selection() applies the changes instantly, d3.transition() applies the changes gradually (and smoothly) over a specified duration.

Whilst we could use a d3.selection() to change the radius of our circle from its current value of $50$ to its target value of $75$ with the following:

circle.attr('r', 75);

We will instead be using a d3.transition() to do the same, but over a duration of $2$ seconds (or $2000$ milliseconds):

circle
    .transition()
    .duration(2000)
    .attr('r', 75);

We can see that we've invoked .transition() on the selection of our <circle> element, specified our transition duration with .duration(), and specified r as the transition to transition to a value of $75$.

Appending to the Container

Finally, let's append everything to our container.

d3
    .select("#container")
    .append(() => svg.node());

We can see the output by checking on our container with the corresponding id, which in this case is where id=container.

Conclusion

If we inspect the HTML, we will see the <svg> and <circle> elements have been added to the <div> where the id=container. We can also see that the <circle> element's r attribute has been set to $75$ after smoothly transitioning from $50$.

<div id="container">
  <svg>
    <circle cx="150" cy="75" r="75"></circle>
  </svg>
</div>

  1. M. Bostock. d3-transition: Animated transitions for D3 selections, https://github.com/d3/d3-transition. 

Grouping Elements

Highlight

The <g> SVG element is a container used to group other SVG elements. Transformations applied to the <g> element are performed on its child elements, and its attributes are inherited by its children. We can create a group element with D3.js by appending a g element using any selection.

Preamble

Let's get access to the D3.js library so that we can begin. In this case, we'll be including the library using the HTML <script> tag.

<script src="https://d3js.org/d3.v6.js"></script>

Introduction

We may often find ourselves needing to group elements together, allowing us to apply transformations or set attributes that are inherited by all child elements of that group. One way to achieve this is to use the container SVG element, <g>, allowing us to arrange our elements into groups which can also be nested1. We can create these group elements using SVG directly, for example with the following.

<svg>
  <g fill="#40F99B">
    <circle cx="85" cy="75" r="50"></circle>
    <circle cx="215" cy="75" r="50"></circle>
  </g>
</svg>

Here we can see that we've changed the colour of both circles by setting the fill attribute on their parent group element, <g>.

But we can also do this with D3.js. In this section, we'll use D3.js to create a group containing two circle elements, and change their fill colour to #40F99B using the group element.

A Container for the Output

This is where you will see the output of the code cells that follow it, provided they are referencing the corresponding id.

<div id="container"></div>

Creating an Empty SVG

We'll create a new detached <svg> element and use the returned selection throughout the rest of this section.

const svg = d3.create("svg");

Creating a Group Element

The <g> SVG element is a container used to group other SVG elements. Transformations applied to the <g> element are performed on its child elements, and its attributes are inherited by its children. We can create a group element with D3.js by appending a g element using any selection.

var shapeGroup = svg.append("g");

Now our shapeGroup variable refers to our selection of the new <g> element.

Creating Elements within a Group

Let's create our circles! This time we're appending these <circle> elements to our new shapeGroup selection of our new group, rather than directly to the <svg> element.

shapeGroup
    .append("circle")
    .attr("cx", 85)
    .attr("cy", 75)
    .attr("r", 50);

shapeGroup
    .append("circle")
    .attr("cx", 215)
    .attr("cy", 75)
    .attr("r", 50);

Setting Group Attributes

Previously, we would set the fill attribute of both <circle> elements to change their colour. With groups, we can instead set the fill attribute of our parent group element, the selection of which is stored in our shapeGroup variable.

shapeGroup
    .attr("fill", "#40F99B");

That's all it takes to change the fill colour of any elements within our group.

Appending to the Container

Finally, let's append everything to our container.

d3
    .select("#container")
    .append(() => svg.node());

We can see the output by checking on our container with the corresponding id, which in this case is where id=container.

Conclusion

If we inspect the HTML, we will see the <svg>, <g>, and <circle> elements have been added to the <div> where the id=container. We can also see that the <g> element's fill colour has been set to #40F99B, which has been inherited by both circles in the output.

<div id="container">
  <svg>
    <g fill="#40F99B">
      <circle cx="85" cy="75" r="50"></circle>
      <circle cx="215" cy="75" r="50"></circle>
    </g>
  </svg>
</div>

  1. W3C. Grouping: the ‘g’ element, https://www.w3.org/TR/SVG/struct.html#Groups. 

Attributes and Styles

Highlight

We can set CSS style properties by invoking .style(name, value) on the selection, and set SVG attributes by invoking .attr(name, value) where the argument to the first parameter should be the name of the attribute we want to set, and the argument to the second parameter should be the value we want to set it to.

Preamble

Let's get access to the D3.js library so that we can begin. In this case, we'll be including the library using the HTML <script> tag.

<script src="https://d3js.org/d3.v6.js"></script>

Introduction

A common way to modify an <svg> element is by setting its attributes. The W3C SVG specification defines two categories of attributes1: the regular category which includes the style attribute for styling with CSS, and the presentation category which includes the fill attribute for painting the interior of an element.

For example, we can change the colour of a circle by setting its fill attribute to #40F99B.

<svg>
  <circle cx="85" cy="75" r="50" fill="#40F99B"></circle>
</svg>

But we can also set these attributes with D3.js. In this section, we'll use D3.js to modify multiple attributes for our SVG elements.

A Container for the Output

This is where you will see the output of the code cells that follow it, provided they are referencing the corresponding id.

<div id="container"></div>

Creating an Empty SVG

We'll create a new detached <svg> element and use the returned selection throughout the rest of this section.

const svg = d3.create("svg");

Creating Elements and Setting Attributes

In previous sections, we've created <circle> elements by invoking d3.append(name) and passing in circle as the argument for the name parameter. This returned a selection which we then used to set the attributes. We can do this by invoking .attr(name, value) on the selection, where the argument to the first parameter should be the name of the attribute we want to set, and the argument to the second parameter should be the value we want to set it to.

Let's set the horizontal and vertical coordinates of our circle with the cx and cy attributes, respectively2. We'll also set the radius of our circle using the r attribute.

var circle = svg
    .append("circle")
    .attr("cx", 85)
    .attr("cy", 75)
    .attr("r", 50);

This time, we'll also change the colour of a circle by setting its fill attribute to #40F99B. We can do this using the selection stored in the circle variable.

circle
    .attr("fill", "#40F99B");

Let's do something similar, but this time for a rect element.

var rect = svg
    .append("rect")
    .attr("x", 165)
    .attr("y", 25)
    .attr("height", 100)
    .attr("width", 100)
    .attr("fill", "#420a91")
    .attr("stroke", "#FF00FF")
    .attr("stroke-width", "4")
    .attr("stroke-dasharray", "10,10");

Here we can see we've created a <rect> element and set its horizontal and vertical coordinates, its height and width, its fill colour, and its stroke. It's worth mentioning that the <rect> element is positioned using the x and y attributes from the top-left corner of the element by default 2, whereas the <circle> element is positioned using the cx and cy attributes from the centre of the element.

Styling with CSS and Setting Styles

We can set CSS style properties by invoking .style(name, value) on the selection, where the argument to the first parameter should be the name of the CSS style property we want to set, and the argument to the second parameter should be the value we want to set it to.

As an example, let's set the background-image CSS property of our <svg> element to a linear gradient going from #420a91 to #40F99B.

svg
    .style("background-image",
           "linear-gradient(to right, #420a91, #40F99B)");

Appending to the Container

Finally, let's append everything to our container.

d3
    .select("#container")
    .append(() => svg.node());

We can see the output by checking on our container with the corresponding id, which in this case is where id=container.

Conclusion

If we inspect the HTML, we will see the <svg>, <circle>, and <rect> elements have been added to the <div> where the id=container. We can also see that the <svg> element's background has been set to a linear-gradient using CSS, and the presentation of the <circle> and <rect> elements have been modified with several SVG attributes.

<div id="container">
  <svg style="background-image: linear-gradient(to right, rgb(66, 10, 145), rgb(64, 249, 155));">
    <circle cx="85" cy="75" r="50" fill="#40F99B"></circle>
    <rect x="165" y="25" height="100" width="100" fill="#420a91" 
          stroke="#FF00FF" stroke-width="4" stroke-dasharray="10,10"></rect>
  </svg>
</div>

In this section, we've demonstrated how to modify elements using attributes and styles using D3.js.


  1. W3C. Appendix G: Attribute Index, https://www.w3.org/TR/SVG/attindex.html. 

  2. W3C. The Circle Element, https://www.w3.org/TR/SVG/shapes.html#CircleElement. 

  3. W3C. The Rect Element, https://www.w3.org/TR/SVG/shapes.html#RectElement. 

Selections and Selecting Elements

Highlight

Besides the d3.create() and d3.append() functions which return selections, we can use the d3.select() and d3.selectAll() functions to return selections by matching a CSS selector.

Preamble

Let's get access to the D3.js library so that we can begin. In this case, we'll be including the library using the HTML <script> tag.

<script src="https://d3js.org/d3.v6.js"></script>