Andreas L.
Andreas L.

Reputation: 4531

Adapt color gradient of folium.plugins.HeatMap() datapoints to provided branca.colormap

Problem description

The lon-lat - data points don't show up on the output graphic/map when creating a costum gradient-dictionary concerning the colors within the folium map-plotting function folium.plugins.HeatMap(). The function-docs can be found here. Is this due to the fact that my gradient-dict is passed with RGBA-color-values instead of bold color-strings such as "blue", "green", etc.? If I don't get the color-ranges adapted to the colormap plotted alongside the actual map, my datapoints will always be displayed in the default manner (which is something like from blue over yellow to red).

As an aside, I would also like to know how to change font-size and color of the branca.colormap.caption and the index-ticklabels. It is always displayed in black with a default fontsize. Sometimes, this isn't really visible against the current map background.

Expected Output

The datapoints of my folium.plugins.HeatMap() are plotted respecting the same colormap as I created via branca.colormaps. I thought this would be possible via the gradient-parameter of folium.plugins.HeatMap().

Moreover, I would like to change the fontsize and -color of the branca.colormap.caption to be better adapted to the actual satellite-map background. The default color "black" is partially intelligible.

Output of print(folium.__version__):

'0.10.1'

Python code with folium and branca implementation - HeatMap with colorscale

# Package importing
import folium
import folium.plugins as fol_plugins


# Instantiate folium base map to plot on
folium_map = folium.Map(location=coord_center_point,
                        zoom_start=zoom_level,
                        max_zoom=max_zoom,
                        tiles=mapbox_tile_URL,
                        attr='Mapbox')

## Add BRANCA colormap ##
import branca.colormap as branca_folium_cm
colormap = branca_folium_cm.linear.Blues_05.scale(z_min, z_max)
colormap.caption = "Bla bla"  # how do I change fontsize and color here?
folium_map.add_child(colormap)

# Prepare gradient dictionary according to the example like {0.4: ‘blue’, 0.65: ‘lime’, 1: ‘red’}
gradient_dict = {}
# Get the index values and colors from the just created branca-colormap
# NOTE: colors are RGBA-vectors, like "(0.9372549019607843, 0.9529411764705882, 1.0, 1.0)":
for ind_val, c in zip(colormap.index, colormap.colors):
    # Create gradient dictionary for heatmap on the fly
    gradient_dict[ind_val] = c

# Resulting gradient dict in my case:
# {1.4117859851611496e-05: (0.9372549019607843, 0.9529411764705882, 1.0, 1.0), 0.00247235752568163: (0.7411764705882353, 0.8431372549019608, 0.9058823529411765, 1.0), 0.004930597191511649: (0.4196078431372549, 0.6823529411764706, 0.8392156862745098, 1.0), 0.007388836857341667: (0.19215686274509805, 0.5098039215686274, 0.7411764705882353, 1.0), 0.009847076523171685: (0.03137254901960784, 0.3176470588235294, 0.611764705882353, 1.0)}

# Overlay the heatmap data on top of the previously instantiated folium basemap
fol_plugins.HeatMap(data=zip(y, x, z),
                    name=titlestr,
                    min_opacity=min_alpha_opacity,
                    max_zoom=max_zoom,
                    radius=radius,
                    gradient=gradient_dict,  # insert gradient dict
                    blur=blur).add_to(folium_map)

Upvotes: 0

Views: 2579

Answers (1)

Andreas L.
Andreas L.

Reputation: 4531

I've found a solution on my own and also an answer in the following discussion on GitHub: https://github.com/python-visualization/folium/issues/1303#issuecomment-675368594

1) Own solution

## * Create a gradient dictionary adapted to the colormap plotted previously * ##
# Instantiate empty dictionary
perc_color_gradient_dict = {}
color_gradient_dict = {}
size_gradient_dict = {}
max_size_val = 12

# Loop over index-values and according colors in RGBA-vector-space
for ind_val, c in zip(colormap.index, colormap.colors):
    # Calculate the percentile of the current index value as the gradient dictionary takes
    # only percentages of the entire value range, not the absolute values themselves
    perc_val = (ind_val - min_z) / ((max_z - min_z))

    # * Get the hex-color, if desired -> PREFERRED
    # NOTE on scope of choosing hex-colors over actual color-names:
    # The colorname space is very small for built-in folium plugins, wherefore only standard names can be passed
    # to the employed functions, e.g. via the color_gradient_dict for adaptation to the colormap
    # -> Here, hex-colors come in handy as they can be interpreted by folium.plugins
    # NOTE on former approach:
    # Get the actual or closest colorname fitting to the current color
    # actual_c_name, closest_c_name = aux_plot.get_exact_or_closest_colorname_to_RGB_color(c)
    hex_color = aux_plot.convert_given_color_to_RGB_RGBA_hex_or_HSV(
        c, return_type="hex", keep_alpha=keep_alpha)
    # Add it to the color_gradient_dicts
    # NOTE on scope: this is only for information purposes, it'll be printed out below
    perc_color_gradient_dict[perc_val] = hex_color
    color_gradient_dict[ind_val] = hex_color
    # Finally, create an size-gradient dictionary alongside with the color dicts
    size_gradient_dict[ind_val] = perc_val * max_size_val


def value_filter_from_dict(val, gradient_dict=None):

    range_limit_keys = np.array(sorted(list(gradient_dict.keys())))

    filtered_array = range_limit_keys[val < range_limit_keys]
    if len(filtered_array) > 0:
        left_border_key = min(filtered_array)
    else:
        left_border_key = max(range_limit_keys)

    return gradient_dict[left_border_key]


# NOTE on implementation syntax:
# If avoiding lambda is a concern, .apply(value_filter_from_dict, gradient_dict=color_gradient_dict) also does the job
# https://stackoverflow.com/questions/52673285/performance-of-pandas-apply-vs-np-vectorize-to-create-new-column-from-existing-c
# -> use key
df['color'] = df[z_varname_new].swifter.apply(
    lambda val: value_filter_from_dict(val, gradient_dict=color_gradient_dict))
df['size'] = df[z_varname_new].swifter.apply(
    lambda val: value_filter_from_dict(val, gradient_dict=size_gradient_dict))

In the following, the printouts for each color-gradient dictionary are displayed.

## * Printout for user * ##
# In:
print(
    f"\nThe final percentage color gradient dictionary is:\n{perc_color_gradient_dict}"
)
print(f"\nThe final color gradient dictionary is:\n{color_gradient_dict}")
print(f"\nThe final size gradient dictionary is:\n{size_gradient_dict}")

# Out:
The final percentage color gradient dictionary is:
{0.0: '#ffffb2', 0.05: '#fff7a4', 0.1: '#ffef97', 0.15000000000000002: '#ffe789', 0.2: '#fedf7c', 0.25: '#fed76e', 0.30000000000000004: '#fecf61', 0.35000000000000003: '#fec559', 0.4: '#febb54', 0.45: '#feb14f', 0.5: '#fda849', 0.55: '#fd9e44', 0.6000000000000001: '#fd943f', 0.65: '#fc873a', 0.7000000000000001: '#f87535', 0.75: '#f36330', 0.8: '#ef502b', 0.85: '#eb3e26', 0.9: '#e72c21', 0.95: '#e31a1c'}

The final color gradient dictionary is:
{1.4304179517249468e-05: '#ffffb2', 0.000263588970541387: '#fff7a4', 0.0005128737615655246: '#ffef97', 0.0007621585525896622: '#ffe789', 0.0010114433436137996: '#fedf7c', 0.001260728134637937: '#fed76e', 0.0015100129256620748: '#fecf61', 0.0017592977166862123: '#fec559', 0.0020085825077103495: '#febb54', 0.002257867298734487: '#feb14f', 0.0025071520897586245: '#fda849', 0.002756436880782762: '#fd9e44', 0.0030057216718069: '#fd943f', 0.0032550064628310373: '#fc873a', 0.003504291253855175: '#f87535', 0.0037535760448793123: '#f36330', 0.00400286083590345: '#ef502b', 0.004252145626927588: '#eb3e26', 0.004501430417951725: '#e72c21', 0.004750715208975863: '#e31a1c'}

The final size gradient dictionary is:
{1.4304179517249468e-05: 0.0, 0.000263588970541387: 0.6000000000000001, 0.0005128737615655246: 1.2000000000000002, 0.0007621585525896622: 1.8000000000000003, 0.0010114433436137996: 2.4000000000000004, 0.001260728134637937: 3.0, 0.0015100129256620748: 3.6000000000000005, 0.0017592977166862123: 4.2, 0.0020085825077103495: 4.800000000000001, 0.002257867298734487: 5.4, 0.0025071520897586245: 6.0, 0.002756436880782762: 6.6000000000000005, 0.0030057216718069: 7.200000000000001, 0.0032550064628310373: 7.800000000000001, 0.003504291253855175: 8.4, 0.0037535760448793123: 9.0, 0.00400286083590345: 9.600000000000001, 0.004252145626927588: 10.2, 0.004501430417951725: 10.8, 0.004750715208975863: 11.399999999999999}

Finally, for the sake of completeness, I'm going to post the details of the previously employed function convert_given_color_to_RGB_RGBA_hex_or_HSV():

def convert_given_color_to_RGB_RGBA_hex_or_HSV(color,
                                               return_type="rgb",
                                               rgb_n_rgba_as_int=False,
                                               keep_alpha=True,
                                               alpha=None):
    """Function documentation:\n
    Accepts the following input and output color formats:
        - HSV (see: https://en.wikipedia.org/wiki/HSL_and_HSV)
        - RGB
        - RGBA
        - hex
        - color-name (string)

    # NOTE on return type preference:
    Prefer matplotlib- over webcolors-return-type within the realm of python

    # NOTE on hex-type as the choice for in-between conversion step:
    -> This is the link between both worlds of matplotlib and webcolors which works for both,
    i.e. which is compatible between both libraries
    """

    # Check whether a correct value has been passed as return-type
    if return_type.lower() not in ["rgb", "rgba", "hsv", "hex"]:
        raise Exception(
            f"The given return type '{return_type}' has not been implemented.")
    else:
        return_type = return_type.lower()  # saves coding later

    # Save initial color for later error-message, if necessary
    original_color = color

    # * i) String
    if isinstance(color, str):
        ## CONVERT to hex-type
        # I) Indicates the case of a hex-color, such as '#08519c'
        if "#" in color:
            pass  # is already of hex-type (compatible with everything)

        # II) Considers percent-string-triplets, such as (u'100%', u'100%', u'100%')
        elif "%" in color:
            # NOTE: use the hex-type as connective link between both libraries
            color = webcolors.rgb_percent_to_hex(color)  # convert to hex-type

        # III) Any other name provided like "black"
        else:
            # NOTE: use the hex-type as connective link between both libraries
            color = webcolors.name_to_hex(color)  # convert to hex-type
            if alpha is not None and keep_alpha:
                color = mpl.colors.to_hex(mpl.colors.to_rgba(color,
                                                             alpha=alpha),
                                          keep_alpha=keep_alpha)

    # * ii) HSV, RGB or RGBA
    else:
        ## CONVERT to hex-type
        # NOTE on keep_alpha: refers to the alpha information of a passed color, like the 4D-RGBA-vectors
        color = mpl.colors.to_hex(color, keep_alpha=keep_alpha)

    ## * 0) Special previous check for hex-type
    if return_type == "hex":
        return color  # is already hex

    ## * 1) Matplotlib
    try:
        # Distinguish between the different return types
        if return_type == "rgb":
            if not rgb_n_rgba_as_int:  # as percent
                return mpl.colors.to_rgb(color)
            else:
                # NOTE: use webcolors for integer RGB-colors without alpha
                # CAUTION: as webcolors cannot handle alpha values, convert possible alpha-hex to normal hex prior to passing them to webcolors-functions
                rgb_as_int = webcolors.hex_to_rgb(
                    mpl.colors.to_hex(mpl.colors.to_rgb(color)))
                return (rgb_as_int.red, rgb_as_int.green, rgb_as_int.blue)

        elif return_type == "rgba":
            if not rgb_n_rgba_as_int:  # as percent
                return mpl.colors.to_rgba(color, alpha=alpha)
            else:
                # NOTE: use webcolors for integer RGB-colors without alpha
                # CAUTION: as webcolors cannot handle alpha values, convert possible alpha-hex to normal hex prior to passing them to webcolors-functions
                rgb_as_int = webcolors.hex_to_rgb(
                    mpl.colors.to_hex(mpl.colors.to_rgb(color)))
                return (rgb_as_int.red, rgb_as_int.green, rgb_as_int.blue,
                        alpha)

        elif return_type == "hsv":
            return mpl.colors.rgb_to_hsv(mpl.colors.to_rgb(color))

    ## * 2) Webcolors
    # NOTE on possible return values: webcolors doesn't have RGBA- and HSV-support
    except:
        # Distinguish between the different return types
        if return_type == "rgb":
            # NOTE: use webcolors for integer RGB-colors without alpha
            # CAUTION: as webcolors cannot handle alpha values, convert possible alpha-hex to normal hex prior to passing them to webcolors-functions
            return webcolors.hex_to_rgb(
                mpl.colors.to_hex(mpl.colors.to_rgb(color)))

    # If this part of the code is reached, something went wrong and it would return None
    raise Exception(
        f"No proper color could be returned for the given color '{original_color}'."
    )

2) Answer from GitHub

From there, I'm going to mention the essential part in the following:

# Get the index values and colors from the just created branca-colormap
# NOTE: colors are RGBA-vectors, like "(0.9372549019607843, 0.9529411764705882, 1.0, 1.0)":
for ind_val, c in zip(colormap.index, colormap.colors):
    # Create gradient dictionary for heatmap on the fly
    r, g, b, a = c
    gradient_dict[ind_val] = f"rgba({r},{g},{b},{a})"

The difference to my initial approach is that I passed the color-tuples directly as a tuple with a length of 4, like e.g. (0.9372549019607843, 0.9529411764705882, 1.0, 1.0). The correct form though is like the f-string mentioned above in the loop constructed from the RGBA - values:

f"rgba({r},{g},{b},{a})"

Upvotes: 1

Related Questions