GenericMappingTools / pygmt

A Python interface for the Generic Mapping Tools.
https://www.pygmt.org
BSD 3-Clause "New" or "Revised" License
768 stars 221 forks source link

Circular colorbar for azimuthal data #3638

Open YaraRossi opened 6 days ago

YaraRossi commented 6 days ago

Description of the desired feature

For various datasets a linear colorbar is appropriate. However when azimuthal data is plotted [0 - 360°] is would make sense to have a circular colorbar that actually represents the direction the data is plotted. This could either be set on the side of the plot or as an inset. At the moment I don't think this exists yet and my only work around would be to save a *.png figure and then add it to the pygmt plot through fig.image() function.

An example is the direction of tilt. It's possible to visualize this with vectors, but when the directions get more heterogeneous it gets messy. This is an example of where the direction of the ground tilt is visualized according to azimuth. The linear colorbar is confusing, as the colors are actually continuous and azimuthal.

Screenshot 2024-11-20 at 14 42 17

the colorbar (colorcircle?) should be something like this:

Screenshot 2024-11-20 at 14 43 26

Are you willing to help implement and maintain this feature?

Yes

welcome[bot] commented 6 days ago

👋 Thanks for opening your first issue here! Please make sure you filled out the template with as much detail as possible. You might also want to take a look at our contributing guidelines and code of conduct.

weiji14 commented 6 days ago

I'm not sure if there's a way to get a circular colorbar, but GMT/PyGMT does have cyclic colormaps listed at https://docs.generic-mapping-tools.org/6.5/reference/cpts.html#id4, and if you use those, there will be a circle/cycle symbol that denotes it is cyclic:

import pygmt

fig = pygmt.Figure()
pygmt.makecpt(cmap="SCM/corkO", series=[0, 360, 45], continuous=True)
fig.colorbar(cmap=True, region=[0,2,4,8], projection="X10c")
fig.show()

produces

cyclic_colorbar

If you want an actual circular colorbar though, that would need to be done in upstream GMT repo https://github.com/GenericMappingTools/gmt, or another workaround.

YaraRossi commented 6 days ago

Thanks for the input. However, I think to understand the data better and faster, and actual circle would be really helpful. I now made a workaround that creates a *.png file with the circular colorbar. That figure can afterwards be inserted into the pygmt figure through fig.image(). Attached is an example file and the figure with the real data. In case anyone else needs the same feature.

Example file: Circular_colormap_example.txt

Figure:

Screenshot 2024-11-20 at 18 36 01
weiji14 commented 6 days ago

Yes, I agree that a circular colorbar would be more appropriate for your case. Also, thanks for sharing your code!

That said, I would advise you to use a proper cyclic colormap, because the current one has a discontinuity at 0 degree / North (the color jumps from green to blue abruptly). Since you are already using a Scientific Colour Map (tofino), you might want to consider one of the cyclic colormaps at https://www.fabiocrameri.ch/colourmaps/ where the transition from 359degrees to 1 degree is smoother. Specifically romaO, bamO, brocO, corkO, or vikO

image

seisman commented 6 days ago

In PyGMT, the circular colorbar can be done by plotting wedges via Figure.plot with style="W".

yvonnefroehlich commented 6 days ago

Some time ago I created a circular colorbar in PyGMT: https://github.com/yvonnefroehlich/gmt-pygmt-plotting/pull/23/files

YaraRossi commented 5 days ago

Thank you weiji14 you are right, that looks much better!

YaraRossi commented 5 days ago

thank you for sharing your code yvonnefroehlich, I'll check it out!

michaelgrund commented 4 days ago

As @seisman mentioned it's also easy to do that by plotting wedges within a 360° range @YaraRossi. Also adding a hole in the center without needing any transparency is straightforward.

Much easier compared to what I did during my PhD :sweat_smile::sweat_smile::sweat_smile: https://github.com/michaelgrund/GMT-plotting/blob/main/009_paper_GR2020/pygmt_jn_fig_s10/GR_2020_Fig_S10.ipynb.

I experimented with wedges the last few weeks and maybe it's worth to consider if we want to implement some high level functions like circular colorbars, pie charts, doughnut plots etc. based on using wedges with plot. I hope I have time to expand my POCs during the holiday season.

Added the potential functions to the list in #2797.

import pygmt
import numpy as np

def colorbar_az(cmap = "romaO", center_x = 1, center_y = 1, radius_inner = 1.5, radius_outer = 2.5):

    values = np.full(360, 1)
    # calc fraction within full circle
    fracs = [value / 360 * 100 for value in values]

    # scale colormap
    pygmt.makecpt(cmap=cmap, series=[0, len(values)-1, 1])

    start_angle = 0

    for i, frac in enumerate(fracs):
        end_angle = start_angle + (frac / 100) * 360
        fig.plot(x = center_x, 
                 y = center_y, 
                 style = f"w{radius_outer}/{start_angle}/{end_angle}+i{radius_inner}", 
                 zvalue = i, 
                 fill = "+z", 
                 cmap = True
                )
        start_angle = end_angle

fig = pygmt.Figure()

fig.coast(
    region=[-125, -122, 47, 49],
    projection="M6c",
    land="grey",
    water="lightblue",
    shorelines=True,
    frame="a",
)

colorbar_az(center_x = -123, center_y = 48)

fig.show()

plot_cbar_az

yvonnefroehlich commented 4 days ago

Just checking this out - nice approach to use wedges (with inner diameter) here! Thanks for sharing your ideas and codes @seisman and @michaelgrund! Would suggest to create a NumPy array with all wedges before plotting to make the code faster (similiar as I did for the rotated recangles, which I use in my code).

import numpy as np
import pygmt

def colorbar_az(cmap="romaO", center_x=0, center_y=0, diameter_inner=1.5, diameter_outer=2.5):

    values = np.full(360, 1)
    # calc fraction within full circle
    fracs = [value / 360 * 100 for value in values]

    # scale colormap
    pygmt.makecpt(cmap=cmap, series=[0, len(values)-1, 1])

    # Create a Numpy array with all wedges
    start_angle = 0
    data_wedges = np.zeros([360, 6])
    for i, frac in enumerate(fracs):
        end_angle = start_angle + (frac / 100) * 360
        data_wedge_temp = np.array([
            center_x, center_y, start_angle, diameter_outer, start_angle, end_angle,
        ])
        data_wedges[i,:] = data_wedge_temp
        start_angle = end_angle

    # Plot all wedges with one call of Figure.plot()
    fig.plot(data=data_wedges, style=f"w+i{diameter_inner}c", cmap=True)
seisman commented 4 days ago

This is another simplified version based on @yvonnefroehlich's script:

import numpy as np
import pygmt

def colorbar_az(cmap="romaO", x0=0, y0=0, diameter_inner=1.5, diameter_outer=2.5, step=1.0):
    pygmt.makecpt(cmap=cmap, series=[0, 360])

    angles = np.arange(0, 360, step)
    # Create the data for the wedges
    data_wedges = np.column_stack([
        np.full_like(angles, x0),  # x
        np.full_like(angles, y0),  # y
        angles,  # For color
        angles,  # start_angle
        angles + step  # end_angle
    ])

    # Plot the all wedges with one call of Figure.plot()
    fig.plot(data=data_wedges, style=f"w{diameter_outer}c+i{diameter_inner}c", cmap=True)

fig = pygmt.Figure()
fig.basemap(region=[-5, 5, -5, 5], frame=True, projection="X10c/10c")
colorbar_az()
fig.show()
YaraRossi commented 4 days ago

I added another variable that allows to rotate the color, so that 0 can be defined where ever people need it. Additionally, it would be great to have lables for example E, N, W, S, but I couldn't manage to locate them next to the wedges automatically. With fig.text() they stayed in the cartesian coordinate system.

import numpy as np
import pygmt

def colorbar_az( fig="fig", cmap="romaO", x0=0, y0=0, diameter_inner=1.5, diameter_outer=2.5, step=1.0,start_azimuth = "0:East, 90:North, 180:West, 270:South"):

    pygmt.makecpt(cmap=cmap, series=[0, 360])

    angles = np.arange(0, 360, step)
    # Create the data for the wedges
    data_wedges = np.column_stack([
        np.full_like(angles, x0),  # x
        np.full_like(angles, y0),  # y
        angles,  # For color
        angles + start_azimuth,  # start_angle
        angles + step + start_azimuth  # end_angle
    ])

    # Plot the all wedges with one call of Figure.plot()
    fig.plot(data=data_wedges, style=f"w{diameter_outer}c+i{diameter_inner}c", cmap=True)