## DEV Community 👩‍💻👨‍💻 is a community of 964,423 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Posted on

# Drawing blocks on the command-line with Python

Hey folks! I just joined the community here, and I thought I'd share an interesting little rabbit hole I recently fell down.

## The goal

A couple of weeks ago, I released progrow; a Python package for drawing progress bars on the command line:

``````apple harvest   █▎                     1 /   9 •  11%
banana harvest  ██                     9 /  99 •   9%
caramel harvest ███████████████████▉ 100 / 100 • 100%
``````

The package does a couple of cool things, but I'm going to focus on my approach to drawing the bar.

## The first shot

My original plan for the bar was just to calculate the amount of space available then fill the correct percentage of it with the Unicode "full block" (`█`) character.

So, for example, a bar of length 10 and percentage 0.5 (with 0.0 being 0% and 1.0 being 100%) would look like this:

``````[█████     ]
``````

Here's some code that fulfills that rule:

``````from decimal import Decimal

EMPTY = " "

def make_bar(length: int, pc: Decimal) -> str:
"""
Returns a string describing a bar "length" characters long
and filled to "pc" percent (0.0 to 1.0).
"""
render = list(EMPTY * length)
# Percentage of the length that each element represents:
element_pc = Decimal(1) / length

for index in range(length):
# Take a slice of the percentage for this element:
block_pc = (pc - (index * element_pc)).min(element_pc)
# Calculate how full this block needs to be:
block_fill_pc = (1 / element_pc) * block_pc
# Add an appropriate character to the render:
render[index] = make_char(block_fill_pc)

return "".join(render)

def make_char(pc: Decimal) -> str:
"""
Gets a character that represents "pc" percent (0.0 - 1.0).
"""
FULL_BLOCK = 0x2588
return EMPTY if pc < Decimal(0.5) else chr(FULL_BLOCK)
``````

So, say I want to draw a bar of length 3 and percentage 0.7. Inside `make_bar()`:

1. `render` is set to a list of 3 spaces.
2. `element_pc` is set to 0.33, since each element in the list represents 33% of the bar area.
3. In iteration `index = 0`:
1. `block_pc` is set to the amount of the bar to be described by the first block. The calculation for this is `(the full bar percentage - the percentage already rendered by previous indexes).min(the maximum percentage that each block can represent)`. That `.min(...)` prevents us from biting off more than we can chew; each block can describe only as much as its maximum, and any remainder needs to be described by subsequent blocks. So, `block_pc` is set to `(0.7 - (0 * 0.33)).min(0.33)`, or 0.33.
2. `block_fill_pc` is set to the percentage full-ness of this block. Each block represents 0.33 of the area and `block_pc` is 0.33, so this block needs to be 1.0 (100%) full.
3. We pass that 1.0 to `make_char()` and -- since `1.0 > 0.5` -- we get a Unicode full block back.
4. In iteration `index = 1`:
1. `block_pc` is set to the amount of the bar to be described by the second block: `(0.7 - (1 * 0.33)).min(0.33)`, or 0.33.
2. `block_fill_pc` is set to the percentage full-ness of this block. Each block represents 0.33 of the area and `block_pc` is 0.33, so this block needs to be 1.0 full.
3. We pass that 1.0 to `make_char()` and -- since `1.0 > 0.5` -- we get a Unicode full block back.
5. In iteration `index = 2`:
1. `block_pc` is set to the amount of the bar to be described by the third block: `(0.7 - (2 * 0.33)).min(0.33)`, or 0.04.
2. `block_fill_pc` is set to the percentage full-ness of this block. Each block represents 0.33 of the area and `block_pc` is 0.04, so this block needs to be 0.12 full.
3. We pass that 0.12 to `make_char()` and -- since `0.12 < 0.5` -- we get an empty space back.
6. Finally, we join `render` together to get the bar as a string containing two Unicode full blocks and an empty string.

We can run this for a number of rows of ever-increasing percentages with code like this:

``````if __name__ == "__main__":
"""
Print a series of rows with ever-increasing percentages.
"""
ROW_COUNT = 11
iteration_pc = (Decimal(1) / (ROW_COUNT - 1))
for index in range(ROW_COUNT):
pc = iteration_pc * index
bar = make_bar(length=6, pc=pc)
print(f"{pc:0.2f}: [{bar}]")
``````

And here's our beautiful output!

``````0.00: [      ]
0.10: [█     ]
0.20: [█     ]
0.30: [██    ]
0.40: [██    ]
0.50: [███   ]
0.60: [████  ]
0.70: [████  ]
0.80: [█████ ]
0.90: [█████ ]
1.00: [██████]
``````

## Well, not THAT beautiful

It's fine, but it's not beautiful. According to those bars, 0.10 and 0.20 are the same, as are 0.30 and 0.40, 0.60 and 0.70, and 0.80 and 0.90.

Wouldn't it be nice to add some more granularity?

Wouldn't be nice if we could draw blocks that were less than a full character width across?

Well, we can!

While the Unicode character `0x2588` represents a full block, there's also a series of Unicode characters that describe eighths of a block:

Hex String Description
`0x2588` `█` Left 8/8 (full block)
`0x2589` `▉` Left 7/8
`0x258A` `▊` Left 6/8
`0x258B` `▋` Left 5/8
`0x258C` `▌` Left 4/8
`0x258D` `▍` Left 3/8
`0x258E` `▎` Left 2/8
`0x258F` `▏` Left 1/8

## Make it beautiful!

So, let's update `make_char()` to return one of these characters instead of a binary "on or off" for each block:

``````from math import ceil

def make_char(pc: Decimal) -> str:
"""
Gets a character that represents "pc" percent (0.0 - 1.0).
"""
eighths = ceil(pc * 8)
return chr(0x2590 - eighths) if eighths > 0 else EMPTY
``````

Essentially, we can calculate how many eighths a percentage is by multiplying it by 8:

• 0.000 (`0.000 * 8`) is 0 eighths.
• 0.125 (`0.125 * 8`) is 1 eighth.
• 0.250 (`0.250 * 8`) is 2 eighths.
• 0.375 (`0.375 * 8`) is 3 eighths.
• 0.500 (`0.500 * 8`) is 4 eighths.
• 0.625 (`0.625 * 8`) is 5 eighths.
• 0.750 (`0.750 * 8`) is 6 eighths.
• 0.875 (`0.875 * 8`) is 7 eighths.
• 1.000 (`1.000 * 8`) is 8 eighths.

When we know how many eighths a percentage is, we can subtract that offset from `0x2590` to get the correct symbol:

• `0x2590 - 1 = 0x258F` (1/8)
• `0x2590 - 2 = 0x258E` (2/8)
• `0x2590 - 3 = 0x258D` (3/8)
• `0x2590 - 4 = 0x258C` (4/8)
• `0x2590 - 5 = 0x258B` (5/8)
• `0x2590 - 6 = 0x258A` (6/8)
• `0x2590 - 7 = 0x2589` (7/8)
• `0x2590 - 8 = 0x2588` (8/8)

Now if we run the code with the updated `make_char()` function:

``````0.00: [      ]
0.10: [▋     ]
0.20: [█▎    ]
0.30: [█▉    ]
0.40: [██▌   ]
0.50: [███   ]
0.60: [███▋  ]
0.70: [████▎ ]
0.80: [████▉ ]
0.90: [█████▌]
1.00: [██████]
``````

Ta-da! Every row now has a much more accurate bar!

In fact, with one quick change to the script, we can create a row for every possible partial block:

``````if __name__ == "__main__":
"""
Print a series of rows with ever-increasing percentages.
"""
LENGTH = 2
ROW_COUNT = (8 * LENGTH) + 1
iteration_pc = (Decimal(1) / (ROW_COUNT - 1))
for index in range(ROW_COUNT):
pc = iteration_pc * index
bar = make_bar(length=LENGTH, pc=pc)
print(f"{pc:0.2f}: [{bar}]")
``````
``````0.00: [  ]
0.06: [▏ ]
0.12: [▎ ]
0.19: [▍ ]
0.25: [▌ ]
0.31: [▋ ]
0.38: [▊ ]
0.44: [▉ ]
0.50: [█ ]
0.56: [█▏]
0.62: [█▎]
0.69: [█▍]
0.75: [█▌]
0.81: [█▋]
0.88: [█▊]
0.94: [█▉]
1.00: [██]
``````

Now, who said command-line tools can't be beautiful?

Featured photo by Wesley Tingey on Unsplash.

DoctorLai

Awesome!

## Update Your DEV Experience Level:

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. 🛠