Black Lives Matter. Please consider donating to Black Girls Code today.

TreeMap ScatterPlot - How to calibrate the asymmetric colorbar

Dear Community,

I’m working on a Tree Map with an asymmetric color scale. While the rectangles represent the size of the samples the colors represents the one period change on a diverging color scale from positive change (blue) to no change (white) and negative change (red).

My question: How can I calibrate the color scale to display Zero in the middle of the scale? I have researched quite a few sources for this, but was not able to find the right code.

Thank you very much for your kind help - hereafter my code snippet with a random number generator.

import pandas as pd
import numpy as np
      
def query_data():
    
    # create dataframe
    df = pd.DataFrame(index=range(20))   
    SeriesName, df['SeriesName'] = [], ''
    for x in range(len(df)):
        SeriesName.append('SeriesName' + '_' + str(x+1))
    df['SeriesName'] = SeriesName  
    df['SeriesSize'] = np.random.randint(10,1000,20)
    df['SeriesChange'] = np.random.randint(-10,40,20)
    
    return df.to_json(date_format='iso', orient='split')

def dataframe():
    return pd.read_json(query_data(), orient='split')

import plotly as py
import plotly.graph_objs as go
import squarify 

import matplotlib.cm as cm
from matplotlib import colors
from matplotlib.colors import LinearSegmentedColormap

# http://nbviewer.jupyter.org/github/empet/Plotly-plots/blob/master/Plotly-asymmetric-colorscales.ipynb
def colormap_to_colorscale(cmap):
    return [ [k*0.1, colors.rgb2hex(cmap(k*0.1))] for k in range(11)]

def colorscale_from_list(alist, name): 
    cmap = LinearSegmentedColormap.from_list(name, alist)
    colorscale=colormap_to_colorscale(cmap)
    return cmap, colorscale

def normalize(x,a,b):
    #if a>=b:
    #    raise ValueError('(a,b) is not an interval')
    return float(x-a)/(b-a)

def asymmetric_colorscale(data,  div_cmap, ref_point=0.0, step=0.05):
    if isinstance(data, pd.DataFrame):
        D = data.values
    elif isinstance(data, np.ma.core.MaskedArray):
        D=np.ma.copy(data)
    else:    
        D=np.asarray(data, dtype=np.float) 

    dmin=np.nanmin(D)
    dmax=np.nanmax(D)
    
    #if not (dmin < ref_point < dmax):
    #    raise ValueError('data is not appropriate for a diverging colormap')
        
    if dmax+dmin > 2.0*ref_point:
        left=2*ref_point-dmax
        right=dmax
        
        s=normalize(dmin, left,right)
        refp_norm=normalize(ref_point, left, right)
        
        T=np.arange(refp_norm, s, -step).tolist()+[s]
        T=T[::-1]+np.arange(refp_norm+step, 1, step).tolist()
        
    else: 
        left=dmin
        right=2*ref_point-dmin
        
        s=normalize(dmax, left,right) 
        refp_norm=normalize(ref_point, left, right)
        
        T=np.arange(refp_norm, 0, -step).tolist()+[0]
        T=T[::-1]+np.arange(refp_norm+step, s, step).tolist()+[s]
        
    L=len(T)
    T_norm=[normalize(T[k],T[0],T[-1]) for k in range(L)] #normalize T values  
    return [[T_norm[k], colors.rgb2hex(div_cmap(T[k]))] for k in range(L)]

# asymmetric colorscale midpoint to zero to assign fillcolor
# https://matplotlib.org/gallery/userdemo/colormap_normalizations.html
class MidpointNormalize(colors.Normalize):
    def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
        self.midpoint = midpoint
        colors.Normalize.__init__(self, vmin, vmax, clip)
    def __call__(self, value, clip=None):
        x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1]
        return np.ma.masked_array(np.interp(value, x, y))
    
RedWhiteBlue=['#FF0D00','#FFFFFF','#0716FF']
fin_cmap, fin_cs = colorscale_from_list(RedWhiteBlue, 'fin_cmap') 

def update_graph_treemap():
    
    dff = dataframe()

    # sort values according to squarify specifications
    dff = dff.sort_values(['SeriesSize'], ascending=False)
    dff = dff.reset_index(drop = True)
    
    dff[['SeriesSize','SeriesChange']]=dff[['SeriesSize','SeriesChange']].apply(lambda x: pd.Series.round(x, 2))
    
    tab = dff['SeriesChange'].tolist()
    fin_asymm_cs = asymmetric_colorscale(tab,  fin_cmap, ref_point=0.0, step=0.05)
 
    norm = MidpointNormalize(midpoint=0., vmin=-max(dff['SeriesChange']), vmax=max(dff['SeriesChange']))    
    colorscale = cm.ScalarMappable(norm=norm, cmap=fin_cmap)
    
    # create fillcolors
    fillcolors=[]
    for x in dff['SeriesChange']:
        fillcolors.append(colors.to_hex(colorscale.to_rgba(x)))
        
    values = []
    values = dff['SeriesName'] + '<br>' + \
        'SeriesSize: ' + dff['SeriesSize'].apply(str) + '<br>' + \
        'SeriesChange: ' + dff['SeriesChange'].apply(str)
    
    # create rectangles
    x = 0.
    y = 0.
    width = 100.
    height = 100.
    
    normed = squarify.normalize_sizes(dff['SeriesSize'], width, height)
    rects = squarify.squarify(normed, x, y, width, height)
    
    shapes = []
    annotations =[]
    counter = 0
    
    for r in rects:
        shapes.append( 
            dict(
                type = 'rect', 
                x0 = r['x'], 
                y0 = r['y'], 
                x1 = r['x']+r['dx'], 
                y1 = r['y']+r['dy'],
                line = dict(width = 2, color = 'white'),
                fillcolor = fillcolors[counter],
                ) 
            )
        if counter < 14:
            annotations.append(
                dict(
                    x = r['x'],
                    y = r['y'] + (r['dy']),
                    text = dff['SeriesName'][counter],
                    showarrow = False,
                    xanchor = 'left',
                    yanchor = 'top',
                )
            )
        counter = counter + 1
            
    figure = {
        'data': [
            go.Scatter(
            x = [ r['x']+(r['dx']/2) for r in rects ], 
            y = [ r['y']+(r['dy']/2) for r in rects ],
            text = values,
            hoverinfo = 'text',
            mode = 'markers',
            marker=dict(
                size=0.1,
                colorscale=fin_asymm_cs,
                cmin = min(dff['SeriesChange']),
                cmax = max(dff['SeriesChange']),
                showscale = True,
                colorbar = dict(
                    len = 1,
                    yanchor = 'middle',
                    outlinecolor = 'white',
                    thickness = 15,
                    ticklen=4
                    )
                )
            )
        ],
        'layout': go.Layout(
            autosize = True,
            xaxis={'showgrid':False, 'zeroline':False, 'showticklabels': False, 'ticks':''},
            yaxis={'showgrid':False, 'zeroline':False, 'showticklabels': False, 'ticks':''},
            shapes=shapes,
            annotations=annotations,
            hovermode='closest',
            )
    }
        
    return figure

py.offline.plot(update_graph_treemap(), filename='squarify-treemap')

Probably best for #api:python

@ock The definition of an asymmetric diverging colorscale is not straightforward.
Here is a Jupyter Notebook that explains how you can derive from a symmetric diverging colorscale an asymmetric one, adapted to your data https://plot.ly/~empet/14918.

Hi empet,

Thank you very much for your reply. I have adjusted the posted code with your suggestions. However, I still needed the MidpointNormalize class to fill the rectangles with some tweaks (vmin = -vmax) to make them compatible to the colorbar. So far the code works fine, but I think that it could be made simpler by using the MidpointNormalize class only. See here: http://chris35wills.github.io/matplotlib_diverging_colorbar/

The current problem is somewhere here:
norm = MidpointNormalize(midpoint=0., vmin=-max(dff[‘SeriesChange’]), vmax=max(dff[‘SeriesChange’]))
colorscale = cm.ScalarMappable(norm=norm, cmap=fin_cmap)

TypeError: Object of type ‘ScalarMappable’ is not JSON serializable

Plan then is to use ‘colorscale’ in the marker dict of go.Scatter.