martijnvermaat / calmap

Calendar heatmaps from Pandas time series data -- See https://github.com/MarvinT/calmap/ for the maintained version
https://pythonhosted.org/calmap
MIT License
211 stars 61 forks source link

How to add a colorbar legend next to a calmap.calendarplot? #9

Closed BreitA closed 3 years ago

BreitA commented 8 years ago

Hi,

I'm currently heavily relying on your module to plot calendar heatmap. Your module is great but I wonder If you know a simple way to put a colorbar for the whole figure on the left of the calendar heatmap figure. It could also be a neat feature in your next iteration on the module to have an option to put automatically the colorbar when creating the heatmap.

martijnvermaat commented 8 years ago

Hi @BreitA,

Yes, this is something that I would like to have. Currently, I don't think it's even possible to do this manually outside of calmap after calling it.

An implementation in yearplot would probably start with something like this:

diff --git a/calmap/__init__.py b/calmap/__init__.py
index 1c53a8c..fddca78 100644
--- a/calmap/__init__.py
+++ b/calmap/__init__.py
@@ -181,7 +181,8 @@ def yearplot(data, year=None, how='sum', vmin=None, vmax=None, cmap='Reds',
     # Draw heatmap.
     kwargs['linewidth'] = linewidth
     kwargs['edgecolors'] = linecolor
-    ax.pcolormesh(plot_data, vmin=vmin, vmax=vmax, cmap=cmap, **kwargs)
+    mesh = ax.pcolormesh(plot_data, vmin=vmin, vmax=vmax, cmap=cmap, **kwargs)
+    ax.figure.colorbar(mesh)

     # Limit heatmap to our data.
     ax.set(xlim=(0, plot_data.shape[1]), ylim=(0, plot_data.shape[0]))

Some quick thoughts:

  1. This would automatically add another axis, which I'm not sure I like from an API point of view (currently, yearplot simply works on one axis, which it returns).
  2. I sense it will be difficult to get the sizing right, especially in the easy "just works" use case.
  3. Needs additional customization options (e.g., via a cbar_kws argument, and by providing an existing axis).
  4. Perhaps this works better on calendarplot than on yearplot?

Not sure when I'll have time to look into this. Would welcome a PR though :)

BreitA commented 8 years ago

I cooked something this evening that works, I will send you what I've done when it's clean.

BreitA commented 8 years ago

Okay so it's very "homemade" but does the job. I'm very bad with matplotlib so I'm sure there is a more elegant way to do the stuff but it works wonders for me atm. as you can see I didt it as an external function to call after the calmap plot. The function add another axis cb_ax that is the colorbar to the figure. This is the copy of my notebook EDIT : update 2016-03-10 a little debug going through after testing it at work + most options tested + a little bit of pep8 ISSUES :

def add_colorbar(data, fig, cmap, auto_tick=True,
                 vmin=None,
                 vmax=None,
                 num_label=3,
                 cb_pos=None,
                 decimal=0,
                 yticks=None,
                 ytick_labels=None,
                 cbar_width=0.01):
    # colorbar position
    if cb_pos is None:
        axes = fig.get_axes()
        if len(axes) > 1:
            pos1 = axes[0].get_position()
            pos2 = axes[-1].get_position()

            cb_pos = [pos1.x1 + 0.05, pos2.y0, cbar_width, pos1.y1 - pos2.y0]
        else:
            cb_pos = [0.9 + 0.05, 0.4, cbar_width, 0.22]  # fine custom pos for the single year plot object

    # custom generation of colorbar (seen in http://matplotlib.org/examples/color/colormaps_reference.html) 
    gradient = np.linspace(0, 255, 256)
    gradient = np.vstack((gradient, gradient))
    # generate colorbar ax
    cb_ax = fig.add_axes(cb_pos)
    cb_ax.imshow(gradient.T, aspect='auto', cmap=plt.get_cmap(cmap))

    # generate tick and tick label by hand (I'm sure there is a much more efficient thing to do here)
    if vmin is None:
        vmin = np.min(data)
    if vmax is None:
        vmax = np.max(data)
    vmax = float(vmax)
    vmin = float(vmin)
    if auto_tick is True:

        step = (vmax - vmin) / (num_label - 1)
        ytick_labels = np.arange(vmin, vmax, step)
        ytick_labels = np.append(ytick_labels, vmax)
        ytick_labels = np.round(ytick_labels, decimal)
        ytick_labels = [str(v) for v in ytick_labels.tolist()]
        yticks = tuple(np.linspace(0, 255, num_label).tolist())

    elif yticks is None or ytick_labels is None:

        raise TypeError('yticks and ytick_labels required if auto_tick is False')
    elif len(yticks) != len(ytick_labels):

        raise TypeError('yticks and ytick_labels must be of same length')
    else:
        # custom ticks
        num_label = len(yticks)
        yticks = np.asarray(yticks) - vmin
        yticks = yticks * 255 / (vmax - vmin)
    # edit ytick + remove xticks  
    cb_ax.set_yticks(yticks)
    cb_ax.set_xticks([])
    cb_ax.set_yticklabels(ytick_labels)
    cb_ax.yaxis.tick_right()
    return cb_ax

#few years example
df=pd.DataFrame(data=np.random.randn(900,1)
                ,index=pd.date_range(start='2014-01-01 00:00:00',freq='1D',periods =900)
                ,columns=['data'])
cmap='RdYlGn'
fig,ax=calmap.calendarplot(df['data'],
                    fillcolor='grey', linewidth=0,cmap=cmap,
                    fig_kws=dict(figsize=(17,8)))
fig.suptitle('Calendar view' ,fontsize=20,y =1.08)

cb_ax=add_colorbar(df['data'],fig,cmap=cmap)

cb_ax.tick_params(axis='y', which='major', labelsize=15)
cb_ax.set_title('rand_data',fontsize=15,y=1.02)      

#few years example custom
df=pd.DataFrame(data=np.random.randn(900,1)
                ,index=pd.date_range(start='2014-01-01 00:00:00',freq='1D',periods =900)
                ,columns=['data'])
cmap='RdYlGn'
yticks=[-5,1,2,4.5]
ytick_labels=['-5 git','1 git','2 git','4.5 git' ]
fig,ax=calmap.calendarplot(df['data'],
                    fillcolor='grey', linewidth=0,cmap=cmap,
                    fig_kws=dict(figsize=(17,8)),vmin=-5,vmax=5)
fig.suptitle('Calendar view' ,fontsize=20,y =1.08)

cb_ax=add_colorbar(df['data'],fig,cmap=cmap,vmin=-5,vmax=5,decimal=1,
                   yticks=yticks,
                   ytick_labels=ytick_labels,
                   auto_tick=False,
                  cbar_width=0.03)

cb_ax.tick_params(axis='y', which='major', labelsize=15)
cb_ax.set_title('rand_data',fontsize=15,y=1.02)       

# lots of years example
df=pd.DataFrame(data=np.random.randn(2000,1)
                ,index=pd.date_range(start='2014-01-01 00:00:00',freq='1D',periods =2000)
                ,columns=['data'])
cmap='RdYlGn'
fig,ax=calmap.calendarplot(df['data'],
                    fillcolor='grey', linewidth=0,cmap=cmap,
                    fig_kws=dict(figsize=(17,17)))
fig.suptitle('Calendar view' ,fontsize=20,y =1.08)

cb_ax=add_colorbar(df['data'],fig,cmap)

cb_ax.tick_params(axis='y', which='major', labelsize=15)
cb_ax.set_title('rand_data',fontsize=15,y=1.02)       

# yearplot test
df=pd.DataFrame(data=np.random.randn(365,1)
                ,index=pd.date_range(start='2014-01-01 00:00:00',freq='1D',periods =365)
                ,columns=['data'])
cmap='RdYlGn'

fig=plt.figure(figsize=(17,8))

ax=calmap.yearplot(df['data'],
                    fillcolor='grey', linewidth=0,cmap=cmap,
                    )

cb_ax=add_colorbar(df['data'],fig,cmap)
cb_ax.tick_params(axis='y', which='major', labelsize=15)
cb_ax.set_title('rand_data',fontsize=15,y=1.08)   
fig.suptitle('Calendar view' ,fontsize=20,y=0.7)

# lots of years example bad scale 
df=pd.DataFrame(data=np.random.randn(2000,1)
                ,index=pd.date_range(start='2014-01-01 00:00:00',freq='1D',periods =2000)
                ,columns=['data'])
cmap='RdYlGn'
fig,ax=calmap.calendarplot(df['data'],
                    fillcolor='grey', linewidth=0,cmap=cmap,
                    fig_kws=dict(figsize=(17,8)))
fig.suptitle('Calendar view' ,fontsize=20,y =1.08)

cb_ax=add_colorbar(df['data'],fig,cmap)

cb_ax.tick_params(axis='y', which='major', labelsize=15)
cb_ax.set_title('rand_data',fontsize=15,y=1.02)       
martijnvermaat commented 8 years ago

@BreitA Thanks a lot for sharing your code.

I don't have the time to work on it now, but I would like to include something like you're showing here in the future. So I'm leaving this issue open, thanks again :+1:

kidpixo commented 8 years ago

Hi, I was searching for the same stuff and I didn't think about looking here in the issues!

I answer to a question on stackoverflow python 2.7 - add a colorbar to a calmap plot and I think it's yours, @BreitA .

My solution is to pick the children of the axis returned by yearplot and stick the colorbar to this mappable : after digging in the code, it is the second call to ax.quadmesh . So the code would be:

fig,ax=calmap.calendarplot(df['data'],
                    fillcolor='grey', linewidth=0,cmap='RdYlGn',
                    fig_kws=dict(figsize=(17,8)))

fig.colorbar(ax[0].get_children()[1], ax=ax.ravel().tolist())

and even simpler for a single yearplot (it is encapsulated in an explicit call to figure to declare the size) :

fig = plt.figure(figsize=(20,8))
ax = fig.add_subplot(111)
cax = calmap.yearplot(df, year=2014, ax=ax, cmap='YlGn')
fig.colorbar(cax.get_children()[1], ax=cax, orientation='horizontal')

A good start would be to return the reference to the actual graphic object that contains the data in the yearplot function

martijnvermaat commented 8 years ago

Thanks @kidpixo for commenting here and linking back to the SO thread!

I'm interested in adding this or making it easier to do it yourself, but time limited at the moment.

kidpixo commented 8 years ago

Thank you for the package, @martijnvermaat !

A solution would be to alter the return of yearplot, I don't know how this impact back compatibility.

I can also think about an helper function, like yearplotcolorbar who takes the output of yearplot and wrap the code I used to generate the plot.

Something more elaborated for calendarplot should do the trick.

BreitA commented 8 years ago

Thanks for the solution provided @kidpixo , will try it when I have the time ! And yes the Stackoverflow was mine and then I ended up asking the question here.

kidpixo commented 8 years ago

@BreitA you are welcome! Could you please accept my solution on stackoverflow?

BreitA commented 8 years ago

Yeah I just did! I tested it and it is exactly what I was looking for and it works great!

kidpixo commented 8 years ago

Nice to hear :-D if you have time to contribute or to publish some code of your solution will be great!

BreitA commented 8 years ago

Honestly at this point your solution is much more elegant and show a better understanding of matplotlib than I have done with my homemade solution.
I will use your solution for my future use of this lib tbh.

Thanks again.

dnaneet commented 4 years ago

> fig = plt.figure(figsize=(20,8))
> ax = fig.add_subplot(111)
> cax = calmap.yearplot(df, year=2014, ax=ax, cmap='YlGn')
> fig.colorbar(cax.get_children()[1], ax=cax, orientation='horizontal')
> ```
> 

This works beautifully!
MarvinT commented 3 years ago

Hi, if this problem still exists and you'd like to create a PR to fix it please direct it to https://github.com/MarvinT/calmap/ That is the version that gets published to pypi and has received several updates to fix some existing issues.

martijnvermaat commented 3 years ago

Thank you for creating the issue. Unfortunately I don't have the time to maintain this project. As per @MarvinT 's comment, please see https://github.com/MarvinT/calmap/ instead.