Data is Beautiful

A practical book on data visualisation that shows you how to create static and interactive visualisations that are engaging and beautiful.

Get the book
Generating Background Colours

Colour makes everything better.

Choosing the right colours can often make all the difference when adding finishing touches to a visualization. Finding colours that work together can be difficult, even more so when we need to find colours for hundreds of elements.

The common solution is to generate the colours randomly or use some existing colour palette. However, we may aim for colours that match their corresponding elements, ultimately leading to a more cohesive design.

So let's see how we can generate some colours with code! There are many ways to generate colours, but we'll be looking at how to extract the dominant colour from an image.

We'll consider one of my earlier Pokémon-themed visualizations made with PlotAPI.

In this visualization, I needed colours for bars that represent each of the 800+ Pokémon. Let's see how I did it, and go a little further by trying some gradients too.

Downloading the images

I've mirrored the images with the following pattern.

https://datacrayon.com/datasets/pokemon_img/{pokedex_id}.png

So we can substitute in a Pokédex ID of 025 to get an image of Pikachu.

All the images are of the PNG file format and have transparent backgrounds, so there's plenty of space available to fill with a nice background colour.

We're going to need to retrieve these images if we want to operate on them. We'll use the urllib package for this.

import urllib.request

image_root = "https://datacrayon.com/datasets/pokemon_img/"

urllib.request.urlretrieve(f"{image_root}025.png", "img_025.png");

Great! We now have Pikachu's image saved locally with the filename img_025.png.

Extracting the dominant colour

We're going to use the Python implementation of the helpful color-thief package. You can install with the following.

pip install colorthief

Let's use it to extract the dominant colour from the image of Pikachu that we just saved locally.

from colorthief import ColorThief

color_thief = ColorThief('./img_025.png')
dominant_color = color_thief.get_color(quality=1)

print(dominant_color)
(228, 200, 119)

We now have the RGB for our dominant colour, but we may want to convert it to a hexadecimal colour string too.

hex_color = f"#{dominant_color[0]:02x}{dominant_color[1]:02x}{dominant_color[2]:02x}"

print(hex_color)
#e4c877

You could do the same with hex_color = '#%02x%02x%02x' % dominant_color, but I prefer using f-strings in my own work.

Playing with colours

Let's use our newly extracted colour as a background for our image.

<div style="height:100px; width:100px; 
            background-color:#dec173;">
    <img style="height:100px; width:100px;" src="img_025.png">
</div>

It's looking great already, but let's try a few variations!

In this next one, let's specify an rgba colour so that we can change the opacity to 0.5.

<div style="height:100px; width:100px; 
            background-color:rgba(222, 193, 115, 0.5);">
    <img style="height:100px; width:100px;" src="img_025.png">
</div>

How about using CSS filters to darken the background colour?

<div style="height:100px; width:100px; position:relative;">
    <div style="position:absolute; height:100px; width:100px; 
                background-color:#dec173; filter:brightness(75%);"></div>
    <img style="position:absolute; height:100px; width:100px; margin:0;"
         src="img_025.png">
</div>

Let's have one last play with filters!

Automating the process

Let's start automating the process. The first thing we'll do is define a function that expects a pokedex_id and returns the image and background as a HTML string.

def get_image(pokedex_id, brightness, size):
    urllib.request.urlretrieve(f"{image_root}{pokedex_id}.png",
                               f"img_{pokedex_id}.png")

    color_thief = ColorThief(f'./img_{pokedex_id}.png')
    dominant_color = color_thief.get_color(quality=1)

    return f'''
        <div style="height:{size}px; width:{size}px;
                    position:relative; float:left; display:block;">
            <div style="position:absolute; height:{size}px; width:{size}px; 
                        background-color:rgb{dominant_color};
                        filter:brightness({brightness}%);"></div>
            <img style="position:absolute; height:{size}px;
                 width:{size}px; margin:0;" src="img_{pokedex_id}.png"
                 class="pub_data_image">
        </div>'''

I've also parameterised the brightness and size so I can play with them later.

I'll be outputting the HTML using IPython.core.display as I'm in a notebook, but you can use whatever you prefer. Let's try displaying a few selections with different configurations to make sure it works.

from IPython.display import display, HTML

html = ""
for i in range(1,10):
    pokedex_id = f"{i:03d}"
    html = html + get_image(pokedex_id, 100, 100)  
      
display(HTML(f'<div style="overflow: auto;">{html}</div>'))

Now with brighter colours.

html = ""
for i in range(1,10):
    pokedex_id = f"{i:03d}"
    html = html + get_image(pokedex_id, 150, 100)  
      
display(HTML(f'<div style="overflow: auto;">{html}</div>'))

Now with smaller images and random brightness.

import random

html = ""
for i in range(1,152):
    pokedex_id = f"{i:03d}"
    html += get_image(pokedex_id,
                      random.randrange(110, 150),
                      50)  
      
display(HTML(f'<div style="overflow: auto;">{html}</div>'))

Now with gradients

To wrap things up I want to share a quick example that blends the backgrounds together using CSS gradients! Let's jump straight into it.

colors = {}

start = 133
end = 137
size = 125
brightness = 125

html = f'''<div style="display:block; margin-left:auto; overflow: auto;
                       margin-right:auto; width:{(end-start)*size}px">'''

for i in range(start,end):
    pokedex_id = f"{i:03d}"
    
    urllib.request.urlretrieve(f"{image_root}{pokedex_id}.png", 
                               f"img_{pokedex_id}.png")
    
    color_thief = ColorThief(f'./img_{pokedex_id}.png')
    colors[i] = color_thief.get_color(quality=1)
    
for i in range(start,end):  
    pokedex_id = f"{i:03d}"
    color_left = colors.get(i - 1, colors[i])
    color_middle = colors[i]
    color_right = colors.get(i + 1, colors[i])
    
    html = html + f'''
    <div style="height:{size}px; width:{size}px;
                position:relative; float:left; display:block;">
        <div style="position:absolute; height:{size}px; width:{size}px; 
                    filter:brightness({brightness}%);
                    background-size: 200% 200%; 
                    background-position: center bottom;
                    background-image:linear-gradient(
                        to right, 
                        rgb{color_left},
                        rgb{color_middle},
                        rgb{color_right});">
        </div>
        <img class="pub_data_image" 
             style="position:absolute; height:{size}px; width:{size}px;
             margin:0;" src="img_{pokedex_id}.png">
    </div>'''
    
html += '</div>'
      
display(HTML(html))

There's so much more we can do with this approach. I'm interested in what you come up with, so feel free to share in the discussion below!

Comments

From the collection

Data is Beautiful

A practical book on data visualisation that shows you how to create static and interactive visualisations that are engaging and beautiful.

Get the book

ISBN

978-1-915907-15-8

Cite

Rostami, S. (2021). Data Is Beautiful. Polyra Publishing.