Search code examples
pythonplotlyhoverheatmapplotly-python

Set plotly tooltip text color the same as the hovered pixel of an heatmap


I'm plotting a quite large heatmap using plotly. I would like to have access to the color of each pixel in the hoverlabel to color a symbol accordingly. Here is what I managed to do so far:

import numpy as np
import plotly.graph_objects as go
import plotly.colors as pc

# generate image
size = 5
img = np.random.random((size, size+2))
# manually generate colors
min_value, max_value = img.min(), img.max()
cmap = pc.sequential.Jet
colors = pc.sample_colorscale(cmap, (img.ravel() - min_value) / (max_value - min_value), colortype="tuple")
# convert to HTML hex color codes and reshape to image shape
colors = np.reshape([f'#{int(c[0]*255):02x}{int(c[1]*255):02x}{int(c[2]*255):02x}' for c in colors], img.shape)
# plot heatmap
fig = go.Figure(
    go.Heatmap(
        z=img,
        colorscale=cmap,
        customdata=colors,
        hovertemplate='Value %{z} with color <span style="color:%{customdata};">⬤</span>',
        name=''
    )
)
fig.show('browser')

heatmap

However, this method uses customdata to pass the array of colors, which is not very convenient as it doubles the final size of the resulting HTML (that can be over 100 or 200 MB).

We know that at some point, the colors are computed by plotly. I would like to know if there is a way to access this in the hoverlabel without having to manually pass them as customdata as this is redundant (something like %{color} for example but this does not exist).


Solution

  • Update: Only color the ⬤, not the entire string

    enter image description here

    It's pretty difficult to see with the darker colors. However, if you only wanted to change the ⬤, this will work.

    Instead of relayout, this is a restyle. (It changes a trace, not the layout.)

    fig = go.Figure(go.Heatmap(z = img, hovertemplate = 'Value %{z} with color <span style="color:red;">⬤</span>'))
    
    pio.write_html(fig, 'index.html', auto_open = True, include_mathjax = 'cdn', include_plotlyjs = 'cdn',
                    full_html = True, div_id = 'myDiv',                           # this id is used in the JS
                    post_script = "setTimeout(function() {" +
                                    "el = document.getElementById('myDiv');" +
                                    "el.on('plotly_hover', function(d) {" +
                                      "cols = el.calcdata[0][0].trace.colorscale; /* get colors by value */" +
                                      "vals = Object.values(cols);" +
                                      "z = d.points[0].z;                         /* collect current hover point data */"  +
                                      "i = [];                                    /* reset i in each event */" +
                                      "vals.forEach((n, index) => {" +
                                        "if(n[0] > z){" +
                                          "i.push(index);                         /* find the right color */" +
                                        "}                                        /* end if */"  +
                                      "});                                        /* end forEach */" +
                                      "/* 2 values for each key, the lower range of z and color */" +
                                      "newC = Object.values(cols)[i[0]][1];       /* extract hex value for color */" +
                                      "labf = d.points[0].fullData.hovertemplate; /* get object to update */" +
                                      "old = labf.match(/(?<=(color:)(.*)(?=;))/)[2];  /* capture color to replace next */"
                                      "labf = labf.replace(old, newC);" +
                                      "Plotly.restyle(el, {'hovertemplate': labf}, d.curveNumber); /* make the change */" +
                                    "});" +
                                   "});")
    

    The JS without the quotes & '+':

    setTimeout(function() {
        el = document.getElementById('myDiv');
        cols = el.calcdata[0][0].trace.colorscale;         /* get colors by value */
        vals = Object.values(cols);
        el.on('plotly_hover', function(d) {
            z = d.points[0].z;                   /* collect current hover point data */
            i = [];                                         /* reset i in each event */
            vals.forEach((n, index) => {
                if(n[0] > z){
                    i.push(index);                          /* find the right color */
                }                                           /* end if */
            })                                              /* end forEach */
                /* 2 values for each key, the lower range of z and color */
            newC = Object.values(cols)[i[0]][1];            /* extract hex value for color */
            labf = d.points[0].fullData.hovertemplate;      /* get object to update */
            old = labf.match(/(?<=(color:)(.*)(?=;))/)[2];  /* capture color to replace next */
            labf = labf.replace(old, newC);
            Plotly.restyle(el, {'hovertemplate': labf}, d.curveNumber) /* make the change */
        });
    });

    ###---- original answer -----

    I think this is what you're looking for.

    enter image description here

    enter image description here

    You mentioned this as an eternal HTML file, and in order to do this, you'll need to at least create the external file. I used plotly.io's write_html so that I could append Javascript with the argument post_script. The file size is 10kb. I don't know what you're using in your file (i.e., data, options, all that), but you may want to look at the arguments I've used in pio.write_html().

    You also mentioned that you did not want to designate colors. I did not designate any specific colors when creating the plot.

    I used np.random.seed(0) so that you could reproduce my example, as well.

    I used comments in my Javascript so that if you wanted to understand what was happening, you could. If you have any questions, let me know.

    import plotly.graph_objects as go
    import plotly.io as pio
    import numpy as np
    
    # generate image
    size = 5
    np.random.seed(0)           # for consistency
    img = np.random.random((size, size+2))
    # manually generate colors
    min_value, max_value = img.min(), img.max()
    
    fig = go.Figure(go.Heatmap(z = img, hovertemplate = 'Value %{z} with color ⬤'))
    
    # fig.show()
    
    pio.write_html(fig, 'index.html', auto_open = True, include_mathjax = 'cdn', include_plotlyjs = 'cdn',
                    full_html = True, div_id = 'myDiv',       # this id is used in the JS
                    post_script = "setTimeout(function() {" +
                                    "el = document.getElementById('myDiv');" +
                                    "el.on('plotly_hover', function(d) {" +
                                      "cols = el.calcdata[0][0].trace.colorscale; /* get colors by value */" +
                                      "vals = Object.values(cols);" +
                                      "z = d.points[0].z;  /* collect current hover point data */"  +
                                      "i = [];             /* reset i in each event */" +
                                      "vals.forEach((n, index) => {" +
                                        "if(n[0] > z){" +
                                          "i.push(index);  /* find the right color */" +
                                        "} /* end if */"  +
                                      "});    /* end forEach */" +
                                      "/* 2 values for each key, the lower range of z and color */" +
                                      "newC = Object.values(cols)[i[0]][1];    /* extract hex value for color */" +
                                      "labf = d.points[0].fullData.hoverlabel; /* get object to update */" +
                                      "labf.font.color = newC;                 /* set font color */" +
                                      "if(z < .5) {            /* find contrasting background color */" + 
                                        "bgc = Object.values(cols)[7][1] /* orangish yellow background for dark colors */" +
                                        "} else {" +
                                        "bgc = Object.values(cols)[1][1] /* dark blueish-purple background for light colors */" +
                                      "}" +
                                      "labf.bgcolor = bgc; /* set bgcolor based on conditions */" + 
                                      "Plotly.relayout(el, {'hoverlabel': labf}, d.curveNumber) /* make the change */" +
                                    "});" +
                                   "});")
    

    The JS without the quotes & '+':

    setTimeout(function() {
        el = document.getElementById('myDiv');
        cols = el.calcdata[0][0].trace.colorscale; /* get colors by value */
        vals = Object.values(cols);
        el.on('plotly_hover', function(d) {
            z = d.points[0].z;  /* collect current hover point data */
            i = [];             /* reset i in each event */
            vals.forEach((n, index) => {
                if(n[0] > z){
                    i.push(index);  /* find the right color */
                } /* end if */
            });    /* end forEach */
                /* 2 values for each key, the lower range of z and color */
            newC = Object.values(cols)[i[0]][1];    /* extract hex value for color */
            labf = d.points[0].fullData.hoverlabel; /* get object to update */
            labf.font.color = newC;                 /* set font color */
            if(z < .5) {            /* find contrasting background color */
                bgc = Object.values(cols)[7][1]; /* orangish yellow background for dark colors */
            } else {
                bgc = Object.values(cols)[1][1]; /* dark blueish-purple background for light colors */
            }
            labf.bgcolor = bgc; /* set bgcolor based on conditions */
            Plotly.relayout(el, {'hoverlabel': labf}, d.curveNumber) /* make the change */
        });
    });