*This post by medical student and data scientist Gabi Maeztu originally appeared in his blog, Merqur.io and is reprinted with his permission.*

After a first attempt of drawing a connectome *(map of neural connections in the brain)*, I came up with a more ambitious idea for a visualization. **Draw one and each of the lines in the square matrix**. We are talking about an average of ±80.000 lines. Bokeh seemed to be performant enough in the last visualization I made, with this one, we will see where the actual limits are.

*This post by medical student and data scientist Gabi Maeztu originally appeared in his blog, Merqur.io and is reprinted with his permission.*

After a first attempt of drawing a connectome *(map of neural connections in the brain)*, I came up with a more ambitious idea for a visualization. **Draw one and each of the lines in the square matrix**. We are talking about an average of ±80.000 lines. Bokeh seemed to be performant enough in the last visualization I made, with this one, we will see where the actual limits are.

### If not dots, then what ?

Arcs, arcs is the answer to that question. If we know the **number of connections in a region** we can figure out the percent that each area **represents in the length of a circumference**; creating each area as an arc scaled with that length, the sum will be a complete circle.

An arc in Bokeh is a glyph and to be drawn needs: `'x', 'y', 'radius', 'start_angle', 'end_angle'`

and `'direction'`

. X and Y are the center of my circle (0,0) and the radius will be 1 to make things easier. The **missing values are start_angle and end_angle** ; with them we will be able to draw the arcs and the circumference.

#### Loading the info

To know the length that each area should have, we need first of all to know the number of connections that each area has.

import numpy as np connectome = np.load('connectome.npy') weights_of_areas = (connectome.sum(axis=0) + connectome.sum(axis=1)) - connectome.diagonal()

Now that we know the number of connections per area, we can know the length that it would represent in a circunference. For that, I will use radians and ** radius = 1** to make operations easier.

from math import pi areas_in_radians = (weights_of_areas/weights_of_areas.sum()) * (2 * pi)

If this is this operation was correct, the sum should be near a 2π radians.

from math import isclose isclose(2*pi, areas_in_radians.sum())

```
True
```

An arc of a circle is a “portion” of the circumference of the circle. The length of an arc is simply the length of its “portion” of the circumference. Now that we now the length of each area in a circumference **we need to know the start and end angles** of the arc in order to be able to draw them. The radian measure, `ø`

, of a central angle of a circle is defined as the ratio of the length of the arc the angle subtends, `s`

, divided by the radius of the circle, `r`

.

So in this case `ø = s`

makes everything much easier. Our points must start on zero, so I add a zero in the begining of the array.

points = np.zeros((areas_in_radians.shape[0]+1)) # We add a zero in the begging for the cumsum points[1:] = areas_in_radians points = points.cumsum()

start_angle = points[:-1] end_angle = points[1:]

Let’s add some color using the function we already created for the previous connectome.

from colorsys import hsv_to_rgb def gen_color(h): golden_ratio = (1 + 5 ** 0.5) / 2 h += golden_ratio h %= 1 return '#{:02X}{:02X}{:02X}'.format(*tuple(int(a*100) for a in hsv_to_rgb(h, 0.55, 2.3)))

colors = np.array([gen_color(area/areas_in_radians.shape[0]) for area in range(areas_in_radians.shape[0])])

#### Creating the arcs

from bokeh.plotting import output_notebook, show, figure, ColumnDataSource from bokeh.models import Range1d

The easiest way is to create a ColumnDataSource instance with the data we need to create the arcs in order to reuse this same instance later on.

arcs = ColumnDataSource( data=dict( x=np.zeros(connectome.shape[0]), y=np.zeros(connectome.shape[0]), start=start_angle, end=end_angle, colors=colors) )

We configure the plot

p = figure(title="Connectomme", outline_line_color="white", toolbar_location="below") # grid configurations p.x_range = Range1d(-1.1, 1.1) p.y_range = Range1d(-1.1, 1.1) p.xaxis.visible = False p.yaxis.visible = False p.xgrid.grid_line_color = None p.ygrid.grid_line_color = None p.arc(x='x', y='y', start_angle='start', end_angle='end', radius=1, line_color='colors', source=arcs, line_width=10)

And print it !

output_notebook(hide_banner=True) show(p)

### I got those arcs, now what ?

`bokeh.crazy_scientific_mode = True`

Now let’s get into the real stuff. **Drawing so many lines is not an easy task**, so I did it using OOP to make it more understandable. I’m sure there is someone reading this and thinking, *“meh, I can do that without declaring a class”* for those; please, do it and share your approach.

We can create each area as an arc with it’s start and end point and the number of connections there should be in that area.

from math import cos, sin class Area(): """ It represents a brain area. It will create a list of available points throught the arc representing that area and then we will use those points as start and end for the beziers. """ def __init__(self, n_conn, start_point, end_point): self.n_conn = n_conn # Number of connections in that area self.start_point = start_point # The start point of the arc representing the area self.end_point = end_point free_points_angles = np.linspace(start_point, end_point, n_conn) # Equally spaced points between start point and end point self.free_points = [[cos(angle), sin(angle)] for angle in free_points_angles] # A list of available X,Y to consume

Now we generate each of the areas in the connectome as an object and store them in a list.

all_areas = [] for i in range(start_angle.shape[0]): all_areas.append(Area(weights_of_areas[i], start_angle[i], end_angle[i]))

Each of those areas we just created has all the points(x,y) that the connections will start from or go to. Those points are in the same space than the arcs we defined earlier. That will create the illusion that the lines are created from the arc, as they will share the same color that the arc they were started from.

Now we have to create each of those lines. The easiest thing is just to loop through all the areas and create each connection.

all_connections = [] for j, region1 in enumerate(connectome): # Get the connections origin region region_a = all_areas[j] color = colors[j] weight = weights_of_areas[j] for k, n_connections_region2 in enumerate(region1): # Get the connection destination region region_b = all_areas[k] for i in range(int(n_connections_region2)): p1 = region_a.free_points.pop() p2 = region_b.free_points.pop() # Get both regions free points and create a connection with the data all_connections.append(p1 + p2 + [color, weight])

len(all_connections)

`4740`

We have `4740`

connections in total that we will draw in the plot. Let’s store them in a DataFrame to easily create a ColumnDataSource later on.

import pandas as pd connections_df = pd.DataFrame(all_connections, dtype=str) connections_df.columns = ["start_x","start_y","end_x","end_y","colors", "weight"]

I store the values in the DataFrame as strings to store as many precission as possible. I found that storing them as floats, made me lose lot of precission and bokeh is taking the string values without problems.

Now we create the curves and add them to the DataFrame

connections_df["cx0"] = connections_df.start_x.astype("float64")/2 connections_df["cy0"] = connections_df.start_y.astype("float64")/2 connections_df["cx1"] = connections_df.end_x.astype("float64")/2 connections_df["cy1"] = connections_df.end_y.astype("float64")/2

We standarize the weights and give them a value that will make them visible. With this, the most important areas will be much more opaque than those with less number of connections.

connections_df.weight = (connections_df.weight.astype("float64")/connections_df.weight.astype("float64").sum()) * 3000

connections_df.head()

start_x | start_y | end_x | end_y | colors | weight | cx0 | cy0 | cx1 | cy1 | |
---|---|---|---|---|---|---|---|---|---|---|

0 | 0.988303 | 0.152505 | 0.96114 | 0.276062 | #6779e5 | 0.093366 | 0.494151 | 0.076253 | 0.480570 | 0.138031 |

1 | 0.988404 | 0.151848 | 0.961324 | 0.275422 | #6779e5 | 0.093366 | 0.494202 | 0.075924 | 0.480662 | 0.137711 |

2 | 0.988505 | 0.15119 | 0.918333 | 0.395809 | #6779e5 | 0.093366 | 0.494252 | 0.075595 | 0.459166 | 0.197905 |

3 | 0.988605 | 0.150532 | -0.644647 | 0.76448 | #6779e5 | 0.093366 | 0.494303 | 0.075266 | -0.322324 | 0.382240 |

4 | 0.988705 | 0.149873 | -0.64414 | 0.764907 | #6779e5 | 0.093366 | 0.494353 | 0.074937 | -0.322070 | 0.382454 |

Once again, using ColumnDataSource I store all the data from the pandas DataFrame in a bokeh compatible object

# Bezier lines beziers = ColumnDataSource(connections_df)

Aaaaaaand let’s plot it

p2 = figure(title="Connectomme", outline_line_color="white", toolbar_location="below") # grid configurations p2.x_range = Range1d(-1.1, 1.1) p2.y_range = Range1d(-1.1, 1.1) p2.xaxis.visible = False p2.yaxis.visible = False p2.xgrid.grid_line_color = None p2.ygrid.grid_line_color = None

Add the glyphs to the plot…

p2.bezier('start_x', 'start_y', 'end_x', 'end_y', 'cx0', 'cy0', 'cx1', 'cy1', source=beziers, line_alpha='weight', line_color='colors') p2.arc(x='x', y='y', start_angle='start', end_angle='end', radius=1, line_color='colors', source=arcs, line_width=10)

```
<bokeh.models.renderers.GlyphRenderer at 0x111a7da58>
```

And Voilá !

show(p2)