Dendrogram

Circular Cluster Heatmap Dark

Circular dendrogram with heatmap-style colored sectors

Output
Circular Cluster Heatmap Dark
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
from matplotlib.colors import LinearSegmentedColormap

np.random.seed(789)

n = 18
labels = [f'V{i}' for i in range(1, n+1)]
data = np.random.rand(n, 5) * 100
values = data.mean(axis=1)
Z = linkage(data, method='ward')

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='#555555', ax=ax_temp)
plt.close(fig_temp)

icoord = np.array(dn['icoord'])
dcoord = np.array(dn['dcoord'])
colors = 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.9 + np.pi * 0.05
    r = y / y_max * 0.5 + 0.45
    return theta, r

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

# Heatmap wedges
cmap = LinearSegmentedColormap.from_list('neon', ['#0a0a0f', '#4927F5', '#27D3F5', '#6CF527', '#F5B027'])
norm_values = (values - values.min()) / (values.max() - values.min())
leaf_positions = np.linspace(x_min, x_max, n)

for i, (pos, val) in enumerate(zip(leaf_positions, norm_values)):
    theta, _ = to_polar(pos, 0)
    theta_deg = np.degrees(theta)
    wedge_width = 360 / n * 0.8
    color = cmap(val)
    wedge = Wedge((0, 0), 0.42, theta_deg - wedge_width/2, theta_deg + wedge_width/2,
                  width=0.12, facecolor=color, edgecolor='#0a0a0f', linewidth=0.5)
    ax.add_patch(wedge)

# Convert to cartesian for ax.plot
def polar_to_cart(theta, r):
    return r * np.cos(theta), r * np.sin(theta)

for ic, dc, color in zip(icoord, dcoord, colors):
    coords = []
    for x, y in zip(ic, dc):
        t, r = to_polar(x, y)
        coords.append((t, r))
    
    # Vertical lines
    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, 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, alpha=0.9)
    
    # Arc
    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, alpha=0.9)

# Labels
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.22 * np.cos(theta), 0.22 * np.sin(theta)
    ax.text(x, y, label, ha=ha, va='center', fontsize=7, color='white',
            rotation=rotation, rotation_mode='anchor')

ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.1)
ax.axis('off')
ax.set_title('Circular Cluster Heatmap', fontsize=14, color='white', 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