Dendrogram

Polar Cluster Bands Light

Light circular dendrogram with colored cluster band indicators

Output
Polar Cluster Bands Light
Python
import matplotlib.pyplot as plt
import numpy as np
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster, set_link_color_palette
from matplotlib.patches import Wedge

np.random.seed(147)

n = 15
labels = [f'P{i}' for i in range(1, n+1)]
data = np.random.rand(n, 4) * 60
Z = linkage(data, method='ward')
clusters = fcluster(Z, t=4, criterion='maxclust')

fig_temp, ax_temp = plt.subplots()
set_link_color_palette(['#F5276C', '#27D3F5', '#6CF527', '#F5B027'])
dn = dendrogram(Z, labels=labels, no_plot=False, color_threshold=0.5*max(Z[:,2]),
                above_threshold_color='#9ca3af', ax=ax_temp)
plt.close(fig_temp)

icoord = np.array(dn['icoord'])
dcoord = np.array(dn['dcoord'])
colors_dn = dn['color_list']

x_min, x_max = icoord.min(), icoord.max()
y_max = dcoord.max()

def to_polar(x, y):
    theta = (x - x_min) / (x_max - x_min) * 2 * np.pi * 0.92 + np.pi * 0.04
    r = y / y_max * 0.45 + 0.5
    return theta, r

fig, ax = plt.subplots(figsize=(10, 10), facecolor='#ffffff')
ax.set_facecolor('#ffffff')
ax.set_aspect('equal')

# Cluster bands
cluster_colors = ['#F5276C', '#27D3F5', '#6CF527', '#F5B027']
leaf_positions = np.linspace(x_min, x_max, n)
reordered_clusters = [clusters[int(dn['ivl'][i].replace('P', ''))-1] for i in range(n)]

for i, (pos, c) in enumerate(zip(leaf_positions, reordered_clusters)):
    theta, _ = to_polar(pos, 0)
    theta_deg = np.degrees(theta)
    wedge_width = 360 / n * 0.85
    color = cluster_colors[(c-1) % len(cluster_colors)]
    wedge = Wedge((0, 0), 0.48, theta_deg - wedge_width/2, theta_deg + wedge_width/2,
                  width=0.08, facecolor=color, alpha=0.25, edgecolor=color, linewidth=1.5)
    ax.add_patch(wedge)

def polar_to_cart(theta, r):
    return r * np.cos(theta), r * np.sin(theta)

for ic, dc, color in zip(icoord, dcoord, colors_dn):
    coords = [(to_polar(x, y)) for x, y in zip(ic, dc)]
    
    x1, y1 = polar_to_cart(coords[0][0], coords[0][1])
    x2, y2 = polar_to_cart(coords[1][0], coords[1][1])
    ax.plot([x1, x2], [y1, y2], color=color, linewidth=2.5, alpha=0.9)
    
    x1, y1 = polar_to_cart(coords[2][0], coords[2][1])
    x2, y2 = polar_to_cart(coords[3][0], coords[3][1])
    ax.plot([x1, x2], [y1, y2], color=color, linewidth=2.5, alpha=0.9)
    
    if coords[1][0] != coords[2][0]:
        arc_thetas = np.linspace(min(coords[1][0], coords[2][0]), max(coords[1][0], coords[2][0]), 40)
        arc_x = [coords[1][1] * np.cos(t) for t in arc_thetas]
        arc_y = [coords[1][1] * np.sin(t) for t in arc_thetas]
        ax.plot(arc_x, arc_y, color=color, linewidth=2.5, alpha=0.9)

for i, (pos, label) in enumerate(zip(leaf_positions, dn['ivl'])):
    theta, _ = to_polar(pos, 0)
    rotation = np.degrees(theta) - 90 if np.pi/2 < theta < 3*np.pi/2 else np.degrees(theta) + 90
    ha = 'right' if np.pi/2 < theta < 3*np.pi/2 else 'left'
    x, y = 0.32 * np.cos(theta), 0.32 * np.sin(theta)
    ax.text(x, y, label, ha=ha, va='center', fontsize=8, color='#374151',
            rotation=rotation, rotation_mode='anchor')
    
    x, y = 0.5 * np.cos(theta), 0.5 * np.sin(theta)
    ax.scatter(x, y, c=cluster_colors[(reordered_clusters[i]-1) % 4], s=40, zorder=5, 
               edgecolor='#ffffff', linewidth=0.8)

ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.1)
ax.axis('off')
ax.set_title('Polar Clustering with Bands', fontsize=14, color='#1f2937', fontweight='bold', y=1.02)

plt.tight_layout()
plt.show()
Library

Matplotlib

Category

Statistical

Did this help you?

Support PyLucid to keep it free & growing

Support