[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[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: Draw a hexagonal grid in Python

[A hexagonal grid drawn with Python]

This example draws a hexagonal grid and displays it inside a ScrolledFrame. For information on the ScrolledFrame, see my post Make a scrolled frame that responds to the mouse wheel in tkinter and Python.

This program must perform two main tasks.

  • Map a row and column to the points that make up the corresponding hexagon
  • Map a point (x, y) to the corresponding row and column

Row/Column to Hexagon Points

The following hex_to_points function maps a row and column to a hexagon's points. Given the hexagon height and a row and column, it returns an array containing six points that you can use to draw the hexagon.

def hex_to_points(xmin, ymin, hex_height, row, col): '''Return the points that define the indicated hexagon.''' # Start with the leftmost corner of the upper left hexagon. wid = hex_height_to_width(hex_height) x = xmin + 0 y = ymin + hex_height / 2 # Move down the required number of rows. y += row * hex_height # If the column is odd, move down half a hex more. if col % 2 == 1: y += hex_height / 2 # Move over for the column number. x += col * (wid * 0.75) # Generate the points. return [ (x, y), (x + wid * 0.25, y - hex_height / 2), (x + wid * 0.75, y - hex_height / 2), (x + wid, y), (x + wid * 0.75, y + hex_height / 2), (x + wid * 0.25, y + hex_height / 2), ]

The code first calls the following hex_height_to_width function to calculate the hexagons' widths. [Calculating a hexagon's width]

def hex_height_to_width(hex_height): '''Return the width of a hexagon with this height.''' return 4 * (hex_height / 2 / math.sqrt(3))

This method uses basic trigonometry to calculate the width of a regular hexagon. The picture on the right shows the geometry. Because the green triangle is a 30-60-90 triangle, the ratio of the short and long sides is 1 / √3. If the longer side has length h / 2 (where h is the hexagon's height), the shorter side has length (h / 2) / √3.

Because the whole hexagon is four times as wide as the green triangle, the total width for the hexagon is 4 × (h / 2) / √3.

[Calculating hexagon position] Having calculated the hexagon's width, the hex_to_points function calculates the X and Y coordinates of the hexagon's leftmost vertex. The Y coordinate is half the height plus the height times the row number. If this is an odd column, the hexagon is shifted down another half of its height.

The X coordinate is 0 plus 3/4 of the hexagon's width.

The picture on the right shows the area allowed for each hexagon in the first row. If you study the hexagons' rectangles and the previous picture, you'll see that the rectangles are 3/4 of the hexagons' widths.

Having calculated the location of the hexagon's leftmost point, it's relatively easy for the method to return an array containing the hexagon's points.

The following draw_hex_grid method uses the hex_to_points method to draw the grid.

def draw_hex_grid(canvas, xmin, ymin, xmax, ymax, hex_height, fill, outline): '''Draw a hexagonal grid for the indicated area.''' # (You might be able to draw the hexagons without # drawing any duplicate edges, but this is a lot easier.) # Loop until a hexagon won't fit. row = 0 while True: # Get the points for the row's first hexagon. points = hex_to_points(xmin, ymin, hex_height, row, 0) # If it doesn't fit, we're done. if points[4][1] >= ymax: break # Draw the row. col = 0 while True: # Get the points for the row's next hexagon. points = hex_to_points(xmin, ymin, hex_height, row, col) # If it doesn't fit horizontally, we're done with this row. if points[3][0] >= xmax: break # If it fits vertically, draw it. if points[4][1] < ymax: canvas.create_polygon(points, fill=fill, outline=outline) # Move to the next column. col += 1 # Move to the next row. row += 1

This method simply loops through rows and columns drawing the corresponding hexagons.

For each row, it gets the points for the first hexagon in the row. The first hexagon sits above the odd-numbered hexagons so if it won't fit then no other hexagons on the row will fit. In that case, the method breaks out of its outer loop and is done.

For the row, the method loops through columns drawing the row's hexagons. If a hexagon won't fit horizontally, the method has finished the row and breaks out of the inner loop so it can draw the next row.

If a hexagon won't fit vertically, that means it's an odd-numbered hexagon that sticks off the bottom edge of the drawing area. In that case the method simply skips that hexagon and continues drawing other even-numbered hexagons to the right.

The following code shows how the program creates its canvas widget and draws the grid.

self.hex_height = 100 num_rows = 6 num_cols = 10 points = hex_to_points(0, 0, self.hex_height, num_rows-1, num_cols-1) canvas_wid = math.ceil(points[3][0]) + 1 canvas_hgt = math.ceil(points[5][1]) + 1 self.canvas = tk.Canvas(my_frame, width=canvas_wid, height=canvas_hgt, borderwidth=0, highlightthickness=0, bg='white') self.canvas.pack() # Draw the grid. draw_hex_grid(self.canvas, xmin=0, ymin=0, xmax=canvas_wid, ymax=canvas_hgt, hex_height=self.hex_height, fill='cornsilk', outline='blue')

This code first uses the hex_to_points function to get the points for a hexagon in position (num_rows - 1, num_cols - 1). It uses the largest X and Y coordinates used by that hexagon to figure out how big to make the program's canvas widget.

The program creates its canvas widget and packs it. It then calls draw_hex_grid to draw the grid.

Point to Row/Column

The following point_to_hex function maps a point to a hexagon's row and column.

def point_to_hex(x, y, hex_height): '''Return the row and column of the hexagon at this point.''' # Find the test rectangle containing the point. hex_width = hex_height_to_width(hex_height) col = int(x / (hex_width * 0.75)) if col % 2 == 0: row = math.floor(y / hex_height) else: row = math.floor((y - hex_height / 2) / hex_height) # Find the test area. testx = col * hex_width * 0.75 testy = row * hex_height if col % 2 != 0: testy += hex_height / 2 # See if the point is above or below the test hexagon on the left. is_above = False is_below = False dx = x - testx if dx < hex_width / 4: dy = y - (testy + hex_height / 2) if dx < 0.001: # The point is on the left edge of the test rectangle. if dy < 0: is_above = True if dy > 0: is_below = True elif dy < 0: # See if the point is above the test hexagon. if -dy / dx > math.sqrt(3): is_above = True else: # See if the point is below the test hexagon. if dy / dx > math.sqrt(3): is_below = True # Adjust the row and column if necessary. if is_above: if col % 2 == 0: row -= 1 col -= 1 elif is_below: if col % 2 != 0: row += 1 col -= 1 return row, col

[Finding the hexagon at a point] The function starts by calculating a candidate row and column. These assume the hexagons lie in rectangles shown in the picture on the right.

If the point lies in the left part of the rectangle, then it may sit above or below the candidate hexagon. (In the little yellow areas inside the rectangle.) The program performs some calculations to see if the point sits above or below the hexagon and updates the row and column if necessary.

When you move the mouse over the canvas widget, the program executes the following code.

def motion(self, event): '''Display the row and column under the mouse.''' row, col = point_to_hex(event.x, event.y, self.hex_height) self.row_col_var.set(f'({row}, {col})')

This code uses the point_to_hex function to get the row and column of the hex under the mouse. It then displays those values in the label above the canvas.

Download the example to experiment with it and to see additional details. In a future post (hopefully soon), I'll modify the example so it can manipulate the hexagons, for example, to let you click on a hex to select it.

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