mwaskom / seaborn

Statistical data visualization in Python
https://seaborn.pydata.org
BSD 3-Clause "New" or "Revised" License
12.63k stars 1.93k forks source link

[New Feature] Range Mapping Plot #3657

Closed ersinesen closed 8 months ago

ersinesen commented 8 months ago

I want to plot mappings between two ranges. For instance, x is the independent variable and f1, f2 corresponding ranges:

x = [1, 2, 3, 4, 5]
f1 = [(0, 20), (20, 40), (40, 60), (60, 80), (80, 100)]
f2 = [(0, 80), (80, 95), (95, 98), (98, 99), (99, 100)]

My basic implementation with pyplot for two cases (bar to bar, bar to circle)

import matplotlib.pyplot as plt

# Data
x = [1, 2, 3, 4, 5]
f1 = [(0, 20), (20, 40), (40, 60), (60, 80), (80, 100)]
f2 = [(0, 80), (80, 95), (95, 98), (98, 99), (99, 100)]

# Define colors for each segment
colors = ['red', 'blue', 'green', 'orange', 'purple']

# Parameters
W = 0.1  # Width of each bar
D = 1    # Distance between bars

# Plotting
fig, ax = plt.subplots(figsize=(8, 6))

# Plotting stacked bars for f1
for i, segment in enumerate(f1):
    ax.bar(-1 * D, segment[1] - segment[0], bottom=segment[0], width=W, color=colors[i])

# Plotting stacked bars for f2
for i, segment in enumerate(f2):
    ax.bar(1 * D, segment[1] - segment[0], bottom=segment[0], width=W, color=colors[i])

# Adding arrows to show mapping
for i in range(len(x)):
    ax.annotate('', xy=(-1 * D + W/2, f1[i][0]), xytext=(1 * D - W/2, f2[i][0]),
                arrowprops=dict(facecolor='black', arrowstyle='-'))
    ax.annotate('', xy=(-1 * D + W/2, f1[i][1]), xytext=(1 * D - W/2, f2[i][1]),
                arrowprops=dict(facecolor='black', arrowstyle='-'))

# Removing grid lines, y-axis label, and axis ticks
ax.grid(False)
ax.set_yticks([])
ax.set_ylabel('')
ax.set_xticks([])

ax.set_title('Range Mapping: Bar to Bar')

plt.show()
import matplotlib.pyplot as plt
import numpy as np

# Data
x = [1, 2, 3, 4, 5]
f1 = [(0, 20), (20, 40), (40, 60), (60, 80), (80, 100)]
f2 = [(0, 80), (80, 95), (95, 98), (98, 99), (99, 100)]

# Define colors for each segment
colors = ['red', 'blue', 'green', 'orange', 'purple']

# Parameters
W = 5  # Width of each bar
D = 10    # Distance between bars
R = 50   # Radius of arcs for f2
arc_width = 5  # Width of the circle arcs

# Plotting
fig, ax = plt.subplots(figsize=(8, 6))

# Plotting stacked bars for f1
for i, segment in enumerate(f1):
    ax.barh(0, segment[1] - segment[0], left=segment[0], height=W, color=colors[i])

# Plotting arcs for f2
theta = np.linspace(0, np.pi, 100)
for i, segment in enumerate(f2):
    start_angle = np.degrees(np.arccos( 2 * segment[0] / 100 - 1 )) 
    end_angle = np.degrees(np.arccos(2 * segment[1] / 100 - 1)) 
    arc_center = (start_angle + end_angle) / 2
    start_x = np.cos(np.radians(start_angle)) * R + R
    start_y = np.sin(np.radians(start_angle)) * R + D
    ax.plot(start_x, start_y, 'o', color='black')
    end_x = np.cos(np.radians(end_angle)) * R + R
    end_y = np.sin(np.radians(end_angle)) * R + D
    ax.plot(end_x, end_y, 'o', color='red')
    ax.plot(np.cos(theta) * R + R, np.sin(theta) * R + D, color='gray')

    # Arrows
    if i == 0:
      ax.annotate('', xy=(f1[i][0], W/2), xytext=(start_x, start_y), arrowprops=dict(facecolor=colors[i], arrowstyle='-'))
    elif i == len(f1) - 1:
      ax.annotate('', xy=(f1[i-1][1], W/2), xytext=(start_x, start_y), arrowprops=dict(facecolor=colors[i], arrowstyle='-'))
      ax.annotate('', xy=(f1[i][1], W/2), xytext=(end_x, end_y), arrowprops=dict(facecolor=colors[i], arrowstyle='-'))
    elif i > 0:
      ax.annotate('', xy=(f1[i-1][1], W/2), xytext=(start_x, start_y), arrowprops=dict(facecolor=colors[i], arrowstyle='-'))

# Removing y-axis
ax.set_yticks([])
ax.set_ylabel('')

# Setting aspect ratio
ax.set_aspect('equal')

# Set xlim to enlarge the graph towards -x
ax.set_xlim(-1 * arc_width, None)

plt.title('Range Mapping: Bar to Circle')

plt.show()

Example at colab

mwaskom commented 8 months ago

Thanks for the suggestion — but this is probably a little bit too narrow to have as a plotting function in seaborn.