Skip to content

[ENH]: Range Mappings #27942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ersinesen opened this issue Mar 18, 2024 · 15 comments
Closed

[ENH]: Range Mappings #27942

ersinesen opened this issue Mar 18, 2024 · 15 comments

Comments

@ersinesen
Copy link

Problem

I want to plot mappings between ranges. For instance x is the independent variable and f1, f2 are 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)]

Proposed solution

Here is my raw implementation for 2 cases: Bar-to-bar, Bar-to-semicircle.

Bar to Bar

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()

Bar to circle

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()

Colab example

@story645
Copy link
Member

Hi can you please add a picture, b/c I'm having trouble visualizing what you're asking for.

If I'm following your feature request correctly, and I'm not sure that I am, I'd be concerned about the ambiguity introduced by supporting pairs. If I am following, please explain how it's an improvement over the ways in which we already support plotting segments:

  1. https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.broken_barh.html#matplotlib.axes.Axes.broken_barh
  2. The bottom keyword of bar https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.bar.html#matplotlib.axes.Axes.bar

And we support donut plots via:

  1. https://matplotlib.org/stable/gallery/pie_and_polar_charts/nested_pie.html#sphx-glr-gallery-pie-and-polar-charts-nested-pie-py
  2. https://matplotlib.org/stable/gallery/pie_and_polar_charts/polar_bar.html#sphx-glr-gallery-pie-and-polar-charts-polar-bar-py

@jklymak
Copy link
Member

jklymak commented Mar 18, 2024

@ersinesen we are pretty conservative about what new methods we will add and support. In general, you need to argue wide possible use, and/or that your method requires operating on Matplotlib at a lower level than the already supported API.

I don't see that the proposal here passes either bar. It seems like a straightforward use of bars with an idiosyncratic labelling standard. This would seem to be fine as a standalone package. You may find https://github.com/matplotlib/matplotlib-extension-cookiecutter useful.

@ersinesen
Copy link
Author

Thanks for the responses. Two visuals are below. I could not find whether donuts have such a mapping mode in addition to inner partitions. I would like to have fancy mapping types, but as said it may not be a major difference. In that case you can close the issue.
bar_to_bar
bar_to_circle

@story645
Copy link
Member

visuals are below. I could not find whether donuts have such a mapping mode in addition to inner partitions. I would like to have fancy mapping types, but as said it may not be a major difference. In that case you can close the issue.

If I'm following, this looks like something you want on annotations? possibly as part of the coordinate system? Could it possibly fall under a usecase of #22223

@timhoffm
Copy link
Member

I think annotations are the wrong tool here. If you want lines, I'd add a LineCollection:

image

import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import numpy as np

bar_width = 0.1
positions = [1, 2]
data = np.array([
    [0, 20, 40, 60, 80, 100],
    [0, 80, 95, 98, 99, 100],
]).T
colors = ['red', 'blue', 'green', 'orange', 'purple']

deltas = np.diff(data, axis=0)

fig, ax = plt.subplots()
for bottom, delta, color in zip(data[:-1], deltas, colors):
    ax.bar(positions, delta, width=bar_width, bottom=bottom, color=color)

left = positions[0]+bar_width/2
right = positions[1]-bar_width/2
lc = LineCollection(
    [[(left, row[0]), (right, row[1])] for row in data],
    color='lightgrey', lw=1)
ax.add_collection(lc)

Alternatively, a stackplot() may also be a suitable visualization:

image

bar_width = 0.1
positions = [1, 2]
data = np.array([
    [0, 20, 40, 60, 80, 100],
    [0, 80, 95, 98, 99, 100],
]).T
colors = ['red', 'blue', 'green', 'orange', 'purple']

deltas = np.diff(data, axis=0)

fig, ax = plt.subplots()
for bottom, delta, color in zip(data[:-1], deltas, colors):
    ax.bar(positions, delta, width=bar_width, bottom=bottom, color=color)

left = positions[0]+bar_width/2
right = positions[1]-bar_width/2
ax.stackplot([left, right], deltas, colors=colors, alpha=0.2)

@timhoffm
Copy link
Member

Note also, that this is an ideal case to write a custom helper function (see also the section "Write your own wrapper function" at https://github.com/timhoffm/using-matplotlib/blob/main/05_Libraries_using_Matplotlib.ipynb).

e.g. for the above LineCollection example:


def connection_plot(ax, data, colors=None, bar_width=0.1):
    if colors is None:
        colors = plt.rcParams['axes.prop_cycle'].by_key()['color'][:len(data)]

    positions = [1, 2]
    deltas = np.diff(data, axis=0)

    for bottom, delta, color in zip(data[:-1], deltas, colors):
        ax.bar(positions, delta, width=bar_width, bottom=bottom, color=color)

    left = positions[0]+bar_width/2
    right = positions[1]-bar_width/2
    lc = LineCollection(
        [[(left, row[0]), (right, row[1])] for row in data],
        color='lightgrey', lw=1)
    ax.add_collection(lc)

so that you for doing a concrete plot you only need to:

data = np.array([
    [0, 20, 40, 60, 80, 100],
    [0, 80, 95, 98, 99, 100],
]).T

fig, ax = plt.subplots()
connection_plot(ax, data)

@jklymak
Copy link
Member

jklymak commented Mar 22, 2024

@timhoffm I thought we had an example of writing a wrapper in our docs, but I can't find it easily. Would you consider adding an example like that?

@timhoffm
Copy link
Member

Sure. I believe it should get a new tile here? https://matplotlib.org/devdocs/users/index.html#users-guide-index

@jklymak
Copy link
Member

jklymak commented Mar 23, 2024

Probably. Maybe the scope of "customizing matplotlib" could start to be expanded beyond just rcParams

@ersinesen
Copy link
Author

I applied @timhoffm 's LineCollection solution to bar-to-circle mapping.

Screen Shot 2024-03-24 at 00 03 11

# Bar to Circle
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import numpy as np

def range_mapping(ax, data, colors=None, bar_width=5, D=5/2):
    if colors is None:
        colors = plt.rcParams['axes.prop_cycle'].by_key()['color'][:len(data)]

    positions = [1]
    deltas = np.diff(data, axis=0)
    R = data[-1,0] / 2
    Theta = data[-1,1]
    theta = np.linspace(0, np.pi, Theta)
    # complete arc
    ax.plot(np.cos(theta) * R + R, np.sin(theta) * R + D, color='gray')
    for pos, delta, color in zip(data[:-1], deltas, colors):
        ax.barh(0, delta[0], left=pos[0], height=bar_width, color=color)
        # Start-end points
        start_angle = pos[1]/Theta * 180 + 180
        end_angle = (pos[1]+delta[1])/Theta * 180 + 180
        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')
        # Arc segment
        arr = theta[::-1]
        theta2 = arr[pos[1]:(pos[1]+delta[1])]
        ax.plot(np.cos(theta2) * R + R, np.sin(theta2) * R + D, color=color)
        # Links
        lc = LineCollection([ [(pos[0], bar_width/2), (start_x, start_y)] ], color='lightgrey', lw=1)
        ax.add_collection(lc)
    
    # Set xlim to enlarge the graph towards -x
    ax.set_xlim(-1 * bar_width, None)

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

  
data = np.array([
    [0, 30, 40, 60, 80, 100],
    [0, 60, 80, 90, 96, 100],
]).T

fig, ax = plt.subplots()
range_mapping(ax, data)

@story645
Copy link
Member

story645 commented Mar 24, 2024

Probably. Maybe the scope of "customizing matplotlib" could start to be expanded beyond just rcParams

Out of scope for this request, but I really think that if that tutorial is expanded then the rcParams documentation (the included rcParams file) should first be pulled out into a standalone document.

@timhoffm
Copy link
Member

Is there anything to do here?

I've put the topic on custom helper functions (#27942 (comment)) in a separate issue : #28248 -> done for the scope of this issue

The only other possible action I see is: Do we want a range mapping example? If so, I think #27942 (comment) is a good candiate. The circle mapping seems a bit too special and can be inferred from the standard bar example.

@jklymak
Copy link
Member

jklymak commented May 17, 2024

Actually we do have a short desc of that, but it takes a little bit to find: https://matplotlib.org/stable/users/explain/quick_start.html#making-a-helper-functions

@jklymak
Copy link
Member

jklymak commented May 17, 2024

I'm going to close this - it is too specialized a visualization with too many permutations on possible data organizations for us to handle in general.

@jklymak jklymak closed this as not planned Won't fix, can't repro, duplicate, stale May 17, 2024
@tacaswell
Copy link
Member

On my to-do list (but it has been there for 3 years) is to move https://github.com/tacaswell/2021-03_APS/tree/main/code/harmonic_oscilator into the main docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants