Path Construction with 2D Arrays

Highlight

Whilst you could say that it's possible to draw a zigzag using multiple rect elements at different positions and rotations, it is certainly an infeasible and inefficient exercise. This is where the SVG path element comes in. A path describes an outline of some shape that can be filled and/or stroked.

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

Up until this section, we've been making use of several basic shapes as they are defined in the W3C specification. These include the rect, circle, and ellipse1. In this section, however, we'll look at how we can create custom and complex shape using the SVG path element2.

Let's use the creation of a zigzag as an example. Whilst you could say that it's possible to draw a zigzag using multiple rect elements at different positions and rotations (in the same way that you could draw any image with an unbounded number of rect elements behaving as pixels), it is certainly an infeasible and inefficient exercise.

This is where the SVG path element comes in. A path describes an outline of some shape that can be filled and/or stroked. Alternatively, a path could be left without a fill or stroke, so that it can be used to position text or define an animation path.

Paths are drawn as if a pen has been placed on paper at a current point, whereby following instructions move that pen in either lines or curves.

<svg>
  <path d="M20,60L60,20L100,60L140,20L180,60L220,20"
        stroke="black" fill="none"></path>
</svg>

We can see the output of the above SVG markup is a zigzag pattern. This shape has been specified using the d (data) property. In this case, we've used a series of moveto (M) and lineto (L) commands to construct our shape using coordinates.

We can see that the fill property has been explicitly set to "none". Otherwise, the default behaviour would be to fill the shape.

As we can see, this is not the desired output for our zigzag shape.

Let's see how we can create the same complex shape using D3.js.

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 Complex Shape with Paths

To create the same zigzag as above we need to complete three steps.

  1. Populate a data structure with our coordinates.
  2. Construct a line generator with d3.line()3.
  3. Generate a line by passing our populated data structure to the line generator.

Let's get started.

Populating a 2D Array

We'll use a simple 2D array for our data structure, passing in the coordinates that specify our zigzag shape. We'll store this in the data variable.

var data = [
    [20, 60],
    [60, 20],
    [100, 60],
    [140, 20],
    [180, 60],
    [220, 20]
];

Constructing the Line Generator

Next, we'll need to construct our line generator using d3.line(). We'll store this in the lineFun variable.

var lineFun = d3.line();

Generate the Path

Then we'll generate the zigzag line by passing our path data into our line generator, i.e. lineFun(data). This will be used to set the d (or data) property of our path element.

To create a <path> element with D3.js we can invoke the d3.append(name) function on our svg selection and pass in the name of the element. We'll also specify the stroke and fill properties.

var line = svg.append("path")
    .attr("d", lineFun(data))
    .attr("stroke", "black")
    .attr("fill", "none");

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 <path> elements have been added to the <div> where the id=container. We can also see that the <path> element's d attribute contains the path data that specifies our zigzag. We also have the stroke set to black, and the fill set to none.

<div id="container">
    <svg>
        <path d="M20,60L60,20L100,60L140,20L180,60L220,20"
        stroke="black" fill="none"></path>
    </svg>
</div>

  1. W3C. Basic Shapes, https://www.w3.org/TR/SVG/shapes.html. 

  2. W3C. Paths, https://www.w3.org/TR/SVG/paths.html. 

  3. M. Bostock. d3-shape: Lines https://github.com/d3/d3-shape#lines. 

MacBook Butterfly Keyboard Problems

Unshaky

Over the last three years, I've occasionally been using a MacBook Pro 13-inch 2018. Unfortunately, it has been the worst laptop experience of my existence!

It was an upgrade to a MacBook Pro 13-inch 2015, and aside from the Apple Touch Bar, it would be fair to consider it superior to its predecessor. However, I started noticing something unusual once I started using it for writing - I kept making mistakes.

Like many, I'm a touch typist and quite comfortable using most keyboards. If I switch to a new keyboard, it sometimes takes some time to get used to the shape of the keys, the spacing between them, and how they feel. So at first, I assumed this unusual feeling came from typing on a brand new keyboard. Over time, I noticed a pattern in the mistakes I was making. Sometimes I would double type a letter, e.g. e, or I would miss the letter entirely.

It starteed gtting on my neervs.

My typing slowed down as I was often typing in anticipation of these mistakes, and it became an overall distraction.

I found similar reports shared online, and I was relieved at the suggestion that it might be a hardware issue with the "butterfly keyboard", rather than a deterioration of my ability!

Through this search, I also discovered an attempt to address the issue with software. It's called Unshaky, and it attempts to dismiss erroneous key press registrations, i.e. those that occur no later than x milliseconds after the previous one. It's not perfect and it doesn't solve all the issues, but in the following screenshot you can see some of what it caught over just a few months:

It has been a terrible experience in general, and I look forward to leaving this machine behind me. What follows is a list of keyboard complaints taken from the Unshaky README1


  1. Unshaky https://github.com/aahung/Unshaky. 

Chord Pro Features For Chord Diagrams

Preamble

In [2]:
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

Colored Diagonals

colored_diagonals=True

This sets the visibility of the occurrences (not co-occurrences), i.e. the representation of the diagonal of the matrix.

In [5]:
from chord import Chord
Chord.user = "hello@shahinrostami.com" 
Chord.key = "CP-1233d274-f968-4018-870b-4926b1793912"

names=['one','two','three','four','five','six']

matrix = [
    [19, 5, 6, 4, 7, 4],
    [5, 4, 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],
]

Chord(matrix, names, title="Diagonals coloured").show()
Chord(matrix, names, title="Diagonals not coloured", colored_diagonals=False).show()
Chord Diagram
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 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).

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 reference2.

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 Costa Rica
5 Hawaii
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 Costa Rica
5 Hawaii
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 Costa Rica Hawaii 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
Costa Rica 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
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 15 0 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0 0 0 0 ... 0 0 44 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 28 0 23 2 0 129 ... 0 0 0 0 0 0 0 0 0 0
13 0 13 3 0 1 0 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 15 0 21 0 19 0 ... 0 0 0 0 0 0 0 0 0 0
16 0 0 0 0 0 44 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 [ ]:
Chord.user = "email here"
Chord.key = "license key here"

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

In [35]:
Chord(
    matrix,
    names,
    colors=colors,
    width=900,
    padding=0.01,
    font_size="12px",
    font_size_large="16px",
    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