7

I'm trying to adapt the following resources to this question:

Python conversion between coordinates

https://matplotlib.org/gallery/pie_and_polar_charts/polar_scatter.html

I can't seem to get the coordinates to transfer the dendrogram shape over to polar coordinates.

Does anyone know how to do this? I know there is an implementation in networkx but that requires building a graph and then using pygraphviz backend to get the positions.

Is there a way to convert dendrogram cartesian coordinates to polar coordinates with matplotlib and numpy?

import requests
from ast import literal_eval
import matplotlib.pyplot as plt
import numpy as np 

def read_url(url):
    r = requests.get(url)
    return r.text

def cartesian_to_polar(x, y):
    rho = np.sqrt(x**2 + y**2)
    phi = np.arctan2(y, x)
    return(rho, phi)

def plot_dendrogram(icoord,dcoord,figsize, polar=False):
    if polar:
        icoord, dcoord = cartesian_to_polar(icoord, dcoord)
    with plt.style.context("seaborn-white"):
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(111, polar=polar)
        for xs, ys in zip(icoord, dcoord):
            ax.plot(xs,ys, color="black")
        ax.set_title(f"Polar= {polar}", fontsize=15)

# Load the dendrogram data
string_data = read_url("https://pastebin.com/raw/f953qgdr").replace("\r","").replace("\n","").replace("\u200b\u200b","")

# Convert it to a dictionary (a subset of the output from scipy.hierarchy.dendrogram)
dendrogram_data = literal_eval(string_data)
icoord = np.asarray(dendrogram_data["icoord"], dtype=float)
dcoord = np.asarray(dendrogram_data["dcoord"], dtype=float)

# Plot the cartesian version
plot_dendrogram(icoord,dcoord, figsize=(8,3), polar=False)

# Plot the polar version
plot_dendrogram(icoord,dcoord, figsize=(5,5), polar=True)

enter image description here

I just tried this and it's closer but still not correct:

import matplotlib.transforms as mtransforms
with plt.style.context("seaborn-white"):
    fig, ax = plt.subplots(figsize=(5,5))
    for xs, ys in zip(icoord, dcoord):
        ax.plot(xs,ys, color="black",transform=trans_offset)

    ax_polar = plt.subplot(111, projection='polar')
    trans_offset = mtransforms.offset_copy(ax_polar.transData, fig=fig)
    for xs, ys in zip(icoord, dcoord):
        ax_polar.plot(xs,ys, color="black",transform=trans_offset)

enter image description here

O.rka
  • 29,847
  • 68
  • 194
  • 309

2 Answers2

10

You can make the "root" of the tree start in the middle and have the leaves outside. You also have to add more points to the "bar" part for it to look nice and round.

We note that each element of icoord and dcoord (I will call this seg) has four points:

seg[1]        seg[2]
+-------------+
|             |
+ seg[0]      + seg[3]

The vertical bars are fine as straight lines between the two points, but we need more points between seg[1] and seg[2] (the horizontal bar, which will need to become an arc).

This function will add more points in those positions and can be called on both xs and ys in the plotting function:

def smoothsegment(seg, Nsmooth=100):
    return np.concatenate([[seg[0]], np.linspace(seg[1], seg[2], Nsmooth), [seg[3]]])

Now we must modify the plotting function to calculate the radial coordinates. Some experimentation has led to the log formula I am using, based on the other answer which also uses log scale. I've left a gap open on the right for the radial labels and done a very rudimentary mapping of the "icoord" coordinates to the radial ones so that the labels correspond to the ones in the rectangular plot. I don't know exactly how to handle the radial dimension. The numbers are correct for the log, but we probably want to map them as well.

def plot_dendrogram(icoord,dcoord,figsize, polar=False):
    if polar:
        dcoord = -np.log(dcoord+1)
        # avoid a wedge over the radial labels
        gap = 0.1
        imax = icoord.max()
        imin = icoord.min()
        icoord = ((icoord - imin)/(imax - imin)*(1-gap) + gap/2)*2*numpy.pi
    with plt.style.context("seaborn-white"):
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(111, polar=polar)
        for xs, ys in zip(icoord, dcoord):
            if polar:
                xs = smoothsegment(xs)
                ys = smoothsegment(ys)
            ax.plot(xs,ys, color="black")
        ax.set_title(f"Polar= {polar}", fontsize=15)
        if polar:
            ax.spines['polar'].set_visible(False)
            ax.set_rlabel_position(0)
            Nxticks = 10
            xticks = np.linspace(gap/2, 1-gap/2, Nxticks)
            ax.set_xticks(xticks*np.pi*2)
            ax.set_xticklabels(np.round(np.linspace(imin, imax, Nxticks)).astype(int))

Which results in the following figure:

Radial dendrogram

chthonicdaemon
  • 19,180
  • 2
  • 52
  • 66
3

First, I think you might benefit from this question.

Then, let's break down the objective: it is not very clear to me what you want to do, but I assume you want to get something that looks like thispolar dendogram

(source, page 14)

To render something like this, you need to be able to render horizontal lines that appear as hemi-circles in polar coordinates. Then, it's a matter of mapping your horizontal lines to polar plot.

First, note that your radius are not normalized in this line:

if polar:
    icoord, dcoord = cartesian_to_polar(icoord, dcoord)

you might normalize them by simply remapping icoord to [0;2pi).

Now, let's try plotting something simpler, instead of your complex plot:

icoord, dcoord = np.meshgrid(np.r_[1:10], np.r_[1:4])

# Plot the cartesian version
plot_dendrogram(icoord, dcoord, figsize=(8, 3), polar=False)

# Plot the polar version
plot_dendrogram(icoord, dcoord, figsize=(5, 5), polar=True)

Result is the following:

regular plot polar plot

as you can see, the polar code does not map horizontal lines to semi-circles, therefore that is not going to work. Let's try with plt.polar instead:

plt.polar(icoord.T, dcoord.T)

produces

plt.polar

which is more like what we need. We need to fix the angles first, and then we shall consider that Y coordinate goes inward (while you probably want it going from center to border). It boils down to this

nic = (icoord.T - icoord.min()) / (icoord.max() - icoord.min())
plt.polar(2 * np.pi * nic, -dcoord.T)

which produces the following

polar dendogram

Which is similar to what you need. Note that straight lines remain straight, and are not replaced with arcs, so you might want to resample them in your for loop.

Also, you might benefit from single color and log-scale to make reading easier

A better polar dendogram

plt.subplots(figsize=(10, 10))
ico = (icoord.T - icoord.min()) / (icoord.max() - icoord.min())
plt.polar(2 * np.pi * ico, -np.log(dcoord.T), 'b')