[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky] [Facebook]
[Build Your Own Python Action Arcade!]

[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Use a rainbow gradient to select numbers in Python

[This program lets you select values from six gradient color maps]

My example Convert between RGB and HSL color models in Python explains how to convert between the RGB (red, green, blue) color model and the HSL (hue, saturation, lightness) model. This example uses some of those techniques to let you select numbers from a rainbow color gradient. Click or click and drag to select a value on any of the gradients.

This example also has some useful helper routines to do things like fill a rectangle with a gradient and convert an HSL color to a string that tkinter can understand.

Drawing Gradients

The following fill_h_rectangle function creates an image that is filled with a left-to-right color gradient like the ones shown in the picture at the top of this post. (The h in the name stands for "horizontal" because the function uses a horizontal gradient.) The function takes as parameters the desired width and height for the image, and the HSL colors that should be used for the left and right edges of the image.

def fill_h_rectangle(wid, hgt, start_hsla, end_hsla): '''Fill the rectangle with a horizontal color gradient.''' # Make an image to draw on. image = Image.new('RGBA', (wid, hgt), 'black') dr = ImageDraw.Draw(image) # Get the differences between the color components. diffs = [end_hsla[i] - start_hsla[i] for i in range(4)] # Color the pixels. for x in range(wid): fraction = x / wid color = [start_hsla[i] + fraction * diffs[i] for i in range(4)] r, g, b, a = hsl_to_rgb(*color) dr.line((x, 0, x, hgt), fill=(r, g, b, a)) return image

This function first creates a new PIL image of the correct size. It then calculates diffs, the differences between the color components of the two values. For example, diffs[0] is the difference between the starting and ending colors' hues.

Next, the code loops through the image's X coordinates. For a given x value, it calculates the fraction of the way the value is through the image. For example, if the image is 100 pixels wide and x is 25, then fraction will be 0.25.

The code then sets color equal to the starting color plus fraction times the diffs values. That sets the color appropriately for the position in the image.

Having calculated the color in the HSL model, the code calls hsl_to_rgb to convert that color into the RGB color model that tkinter understands. It the uses the RGB color to draw a vertical line with the current X coordinate.

Updating hsl_to_rgb

The last few examples used the hsl_to_rgb function to convert from HSL to RGB. Neither of those models change a pixel's alpha (opacity) value, so that function did not take a alpha parameter. That made the code a bit awkward because it needed to handle the alpha component separately.

In this example, I decided to clean things up a bit so the program can pass alpha into the hsl_to_rgb function, which leaves that value unchanged. Here's the new version of the function with the changes highlighted in blue.

def hsl_to_rgb(h, s, l, a): '''Convert an HSL value into an RGB value.''' if l <= 0.5: p2 = l * (1 + s) else: p2 = l + s - l * s p1 = 2 * l - p2 if math.isclose(s, 0): r = l g = l b = l else: r = qqh_to_rgb(p1, p2, h + 120) g = qqh_to_rgb(p1, p2, h) b = qqh_to_rgb(p1, p2, h - 120) # Convert RGB into the 0 to 255 range. r = round(r * 255) g = round(g * 255) b = round(b * 255) a = round(a) # Round a but leave it otherwise unchanged. return r, g, b, a

The only non-trivial difference is that this version rounds a because RGB colors should have integer components. Otherwise, a is unchanged.

Now the fill_h_rectangle method can call the function like this so it can handle the alpha component like the others.

r, g, b, a = hsl_to_rgb(*color)

For completeness, I made similar changes to preserve the alpha value in the rgb_to_hsl function, although this program doesn't use that function.

Building the User Interface

The main app's build_ui method shown in the following code creates the tkinter user interface.

def build_ui(self): '''Build the user interface.''' wid = 300 hgt = 40 # Lists to hold key objects. self.canvases = [] self.photo_images = [] self.vars = [] self.swatches = [] self.reticles = [] # Define color ranges. self.color_pairs = [ [[ 0, 1.0, 0.5, 255], [360, 1.0, 0.5, 255]], # Red to Red. [[ 60, 1.0, 0.5, 255], [ 0, 1.0, 0.5, 255]], # Yellow to Red. [[240, 1.0, 0.5, 255], [600, 1.0, 0.5, 255]], # Blue to Blue. [[120, 1.0, 1.0, 255], [120, 1.0, 0.0, 255]], # Green Lightness.. [[240, 0.0, 0.5, 255], [240, 1.0, 0.5, 255]], # Blue Saturation. [[ 0, 0.5, 0.7, 255], [240, 0.5, 0.7, 255]], # Pastels. ] label_text = ['Red to Red', 'Yellow to Red', 'Blue to Blue', 'Green Lightness', 'Blue Saturation', 'Pastels'] # Make a 1x1 pixel image to force labels to measure in pixels. self.pixel = tk.PhotoImage(width=1, height=1) # Create the rows of widgets. for color_pair in self.color_pairs: index = len(self.canvases) label = tk.Label(self.window, text=label_text[index]) label.pack(side=tk.TOP, anchor=tk.W, padx=5) frame = tk.Frame(self.window) frame.pack(side=tk.TOP, padx=10, pady=(0,10)) pil_image = fill_h_rectangle(wid, hgt, color_pair[0], color_pair[1]) canvas = tk.Canvas(frame, width=wid, height=hgt, highlightthickness=0) photo_image = ImageTk.PhotoImage(pil_image) canvas.create_image(0, 0, anchor=tk.NW, image=photo_image) canvas.pack(side=tk.LEFT) var = tk.StringVar(value='0.00') entry = tk.Entry(frame, textvariable=var, width=4, state='readonly', justify=tk.RIGHT) entry.pack(side=tk.LEFT, padx=5) swatch = tk.Label(frame, width=hgt, height=hgt, image=self.pixel, compound=tk.CENTER) swatch.pack(side=tk.LEFT) canvas.bind('<Button-1>', lambda event, i=index: self.track_mouse(event, i)) canvas.bind('<B1-Motion>', lambda event, i=index: self.track_mouse(event, i)) # Draw the initial reticle. self.reticles.append( canvas.create_polygon(make_reticle_points(0), fill='white', outline='black')) # Save object we'll need later. self.canvases.append(canvas) self.photo_images.append(photo_image) self.vars.append(var) self.swatches.append(swatch) # Display the initial color sample. swatch['background'] = hsl_to_tkinter(color_pair[0])

The code first sets wid and hgt to the desired width and height of the color gradient areas. It then defines a bunch of lists that the code uses to store important objects.

Next, the code makes lists holding pairs of colors and titles for the different gradients. In each color pair, the first color is a gradient's start color and the second color is the gradient's end color.

The method then creates a 1×1 pixel image that it will use to trick Label widgets into sizing themselves in pixels. You'll see how that works shortly.

The code then loops through the color pairs. For each pair, it creates a label showing the name of the gradient. It then calls fill_h_rectangle to make a gradient between the two colors, makes a Canvas widget, and displays the gradient image in the Canvas. It also creates an Entry widget where it can later display numeric values.

Next, the code creates a Label that it will use to display a sample of the selected color. To make things line up nicely, I want to size this label in pixels, but Label widgets normally set their sizes in character widths and heights so you have very little control over the size. It turns out if you set a Label widget's image property to an image, the widget switches to setting its size in pixels. Yes, this is silly and inconsistent, but that's the way it works.

The loop then binds the mouse down and mouse move events to the self.track_mouse method. It passes into that method the mouse event object and the index of the gradient that produced the event. Later the code will use that index to update the selected numeric value.

Next, the code calls make_reticle_points to get the points needed to draw a little triangle on the Canvas widget's top (more on that shortly). It draws the reticle and saves its ID in the self.reticles list.

The code then saves key objects in the app's lists for later use.

Finally, it uses the hsl_to_tkinter function to convert the color pair's starting color into something tkinter can understand and sets the swatch label's background color to the result.

Converting HSL to tkinter

Here's the hsl_to_tkinter helper function that converts a color from HSL to something tkinter can understand.

def hsl_to_tkinter(color): '''Convert an HSL color into a tkinter color.''' r, g, b, a = hsl_to_rgb(*color) return f'#{r:02x}{g:02x}{b:02x}'

This code calls hsl_to_rgb to convert the color from HSL to RGB. It then converts the result into a string of the form #rrggbbaa that tkinter can understand and returns it.

Drawing Reticles

The following function returns the points needed to draw the little rectangles at the top of the color gradients shown in the picture at the top of the post.

def make_reticle_points(x): '''Make a triangle with base y = 0 centered horizontally at x.''' r = 5 return [(x - r, 0), (x + r, 0), (x, r)]

This code is straightforward. It just makes the points needed to draw the triangle centered with the given X coordinate. The value r determines how big the triangle is.

Tracking Mouse Movements

The following event handler executes when you click or click and drag on a color gradient.

def track_mouse(self, event, index): '''Update the appropriate display.''' # Get the fraction of the way through the gradient this point is. wid = self.photo_images[index].width() fraction, x = x_coord_to_fraction(event.x, 0, wid - 1) # Display the hue. self.vars[index].set(f'{fraction:.2f}') # Update the reticle. self.canvases[index].coords(self.reticles[index], make_reticle_points(x)) # Convert the number into a color. hsl_color = number_to_color(fraction, *self.color_pairs[index]) # Display a color sample. self.swatches[index]['background'] = hsl_to_tkinter(hsl_color)

This function gets the gradient's width. It then calls x_coord_to_fraction (described shortly) to convert the X coordinate into a fraction of the distance across the gradient. (The code subtracts 1 from the width because the rightmost pixels have X coordinate equal to 1 less than the width.)

The code then sets the self.vars value for this gradient to update the text display. It calls number_to_color (also described shortly) to get the color that corresponds to the mouse's position in the gradient. Finally, it uses hsl_to_tkinter to convert that color into tkinter format and sets the gradient's color swatch label to display a sample of that color.

Here's the x_coord_to_fraction function.

def x_coord_to_fraction(x, xmin, xmax): '''Convert x into the fraction of the way between xmin and xmax.''' if x < xmin: x = xmin if x > xmax: x = xmax # Return the fraction and the revised x value. return (x - xmin) / (xmax - xmin), x

This program uses the function to map X positions between 0 and with width of the gradient minus one, but this function can map between any lower and upper bounds. It starts by ensuring that x lies between the minimum and maximum values. It then calculates the fraction of the x position between the two bounds and returns it.

The following code shows the number_to_color function.

def number_to_color(fraction, hsl_color1, hsl_color2): '''Convert fraction (between 0 and 1) to a color in the color range.''' return [hsl_color1[i] + fraction * (hsl_color2[i] - hsl_color1[i]) for i in range(4)]

This function calculates the color a given fraction of the distance from a start color to an end color. For example, if the start and end colors are red and green and fraction is 0.5, then the result is yellow because yellow is halfway between red and green.

To calculate the color, the function adds the start color to fraction times the difference between the start and end colors. The code uses a list comprehension to perform the calculation on each of the color's components. [A heat map showing rainfall in Europe during June 2022]

Conclusion

This example lets you use a color gradient to select a number. These kinds of gradients are also useful for displaying heat maps where you use colors to represent various values on a two-dimensional image. For example, the picture on the right shows a heat map representing rainfall in Europe during June 2022. The color gradient shows rainfall on a logarithmic scale ranging from brown (no rain) to purple (800+ mm of rain).

Download the example to experiment with it and to see additional details.

© 2025 Rocky Mountain Computer Consulting, Inc. All rights reserved.