Dendrogram

Real Circular Dendrogram Dark

True circular dendrogram using scipy icoord/dcoord transformation to polar coordinates

Output
Real Circular Dendrogram Dark
Python
import matplotlib.pyplot as plt
import numpy as np
from scipy.cluster.hierarchy import dendrogram, linkage, set_link_color_palette

np.random.seed(42)

# Generate hierarchical data
n_samples = 20
labels = [f'S{i}' for i in range(1, n_samples + 1)]
data = np.random.rand(n_samples, 5) * 100
Z = linkage(data, method='ward')

# Get dendrogram coordinates (invisible figure)
fig_temp, ax_temp = plt.subplots()
set_link_color_palette(['#F5276C', '#27D3F5', '#6CF527', '#F5B027', '#4927F5'])
dn = dendrogram(Z, no_plot=False, color_threshold=0.7*max(Z[:,2]), 
                above_threshold_color='#555555', ax=ax_temp)
plt.close(fig_temp)

# Extract coordinates
icoord = np.array(dn['icoord'])
dcoord = np.array(dn['dcoord'])
colors = dn['color_list']

# Normalize coordinates for polar transformation
x_min, x_max = icoord.min(), icoord.max()
y_max = dcoord.max()

# Map x to angle (0 to 2*pi), y to radius
def to_polar(x, y):
    theta = (x - x_min) / (x_max - x_min) * 2 * np.pi * 0.95 + np.pi * 0.025
    r = y / y_max * 0.7 + 0.25
    return theta, r

# Create polar plot
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw={'projection': 'polar'}, facecolor='#0a0a0f')
ax.set_facecolor('#0a0a0f')

# Draw each U-shaped link
for i, (ic, dc, color) in enumerate(zip(icoord, dcoord, colors)):
    # Each link has 4 points forming a U shape
    thetas, rs = [], []
    for x, y in zip(ic, dc):
        t, r = to_polar(x, y)
        thetas.append(t)
        rs.append(r)
    
    # Draw the U with smooth arcs for horizontal parts
    # Vertical parts
    ax.plot([thetas[0], thetas[1]], [rs[0], rs[1]], color=color, linewidth=2, alpha=0.9)
    ax.plot([thetas[2], thetas[3]], [rs[2], rs[3]], color=color, linewidth=2, alpha=0.9)
    
    # Horizontal arc at the top
    if thetas[1] != thetas[2]:
        arc_thetas = np.linspace(min(thetas[1], thetas[2]), max(thetas[1], thetas[2]), 30)
        arc_rs = [rs[1]] * len(arc_thetas)
        ax.plot(arc_thetas, arc_rs, color=color, linewidth=2, alpha=0.9)

# Add leaf labels
leaf_positions = np.linspace(x_min, x_max, n_samples)
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'
    ax.text(theta, 0.18, label, ha=ha, va='center', fontsize=8, color='white', 
            rotation=rotation, rotation_mode='anchor')

# Add leaf markers
for i, pos in enumerate(leaf_positions):
    theta, _ = to_polar(pos, 0)
    color = ['#F5276C', '#27D3F5', '#6CF527', '#F5B027', '#4927F5'][i % 5]
    ax.scatter(theta, 0.25, c=color, s=40, zorder=5, edgecolor='white', linewidth=0.5)

ax.set_ylim(0, 1.05)
ax.set_yticklabels([])
ax.set_xticklabels([])
ax.spines['polar'].set_visible(False)
ax.grid(True, color='#333333', alpha=0.3, linestyle='--')

ax.set_title('Circular Hierarchical Clustering', fontsize=14, color='white', fontweight='bold', y=1.08)

plt.tight_layout()
plt.show()
Library

Matplotlib

Category

Statistical

Did this help you?

Support PyLucid to keep it free & growing

Support