Creating a prototype library for visualizing algorithms in Python

One day I decided to work with various algorithms, but as it turned out it was not so simple. The fact is that it is easier to perceive information visually than in the form of code. Then I set myself a goal – to try to write a small but useful prototype of a library for visualizing algorithms in the Python programming language.

Justification of relevance

When studying algorithms, it is best to familiarize yourself not only with their description and possible implementations in general, but also with how they work on specific data. This need is partially covered by a debugger, but it is much more convenient to look at a GIF image, which displays only useful information requested by the user. Algorithm visualization is often more useful than a traditional debugger in certain contexts due to its ability to provide a high-level, intuitive view of the entire algorithmic process. Although debuggers are excellent at identifying and solving specific problems in code, they may fail to understand the overall flow and logic of complex algorithms. Algorithm visualization offers a holistic view, allowing developers to observe the sequential execution of each step and observe data transformations in a graphical format. This visual clarity is especially useful for understanding complex algorithms with multiple decision points and complex branching. Unlike debuggers, which focus on isolating errors, algorithm visualization allows developers to understand the behavior of the algorithm as a whole, helping both to identify inefficiencies and to optimize the overall structure of the algorithm. This broader perspective can play an important role in creating more robust and efficient algorithms in various computing applications.

Set goals and objectives

The goal of my mini-project is to create a prototype of a library with which it would be convenient to create animations of algorithms. To implement it, the following tasks are solved:

  1. Understand OOP in python, master working with classes and “magic methods”;

  2. Understand the Pillow library, understand how to create individual images and gif animations;

  3. Animate the rearrangement and assignment of elements of a one-dimensional array;

  4. Test the library's operation using simple algorithms.

Road map

In order to understand the material as accurately as possible and not get lost, I created a road map. It can be divided into 3 parts:

  1. Magic methods (init, setitem, getitem)

Magic methods are special Python methods that begin and end with double underscores. These methods allow you to determine how objects behave under different circumstances. Method init is the most important magic method in Python, serving as a class constructor. It is automatically called when an object is created from a class and is used to initialize its attributes.
Methods setitem And getitem are the magic methods associated with indexing and subscription in Python. They are used when instances of a class are treated as containers or mappings. Method setitem called when an element is given a specific index, and the method getitem Called when an element is accessed using an index.

  1. Pillow Library

A library prototype is developed using the Pillow library [4]. In this case, some functions and methods were used to solve three specific problems:

  1. Creating a new image;

  2. Drawing over an image (including text);

  3. Saving an array of images as .gif animation. Below is an example script that creates a new image:

self.data[index] = value

width = (cellSize + space + 2) * len(self.data)
height = cellSize * 6

dx = cellSize
dy = cellSize / 2

img = Image.new('RGB', (width, height), (255, 255, 255))
draw = ImageDraw.Draw(img)

font = ImageFont.truetype("arial.ttf", 15)

for i, n in enumerate(self.data):
    draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                    50 - cellSize / 2 + dy,
                    i * cellSize + cellSize / 2 + i * space + dx,
                    50 + cellSize / 2 + dy],
                    fill = None, outline = (0, 0, 0))

    if n != None:
        draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                    50 - cellSize / 2 + dy],
                    text = str(n), fill = (0, 0, 0), font = font)
  1. Animation: text description, formula output

The figure below shows a diagram according to which an image of the array is formed. space is the distance between the squares on the right and left, cellSize is the side of the square. The height of a gif image is cellSize*5, and the length already depends on the number of squares, that is, on the number of numbers in the array.

Array image diagram

Array image diagram

This layout allows you to create the following formulas:

draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                50 - cellSize / 2 + dy,
                i * cellSize + cellSize / 2 + i * space + dx,
                50 + cellSize / 2 + dy],
                fill = None, outline = (0, 0, 0))
draw.text([i * cellSize - cellSize / 2 + i * space + dx,
            50 - cellSize / 2 + dy],
            text = str(n), fill = (0, 0, 0), font = font)

The figure below shows an animation layout according to which the red and green squares move. Since the number in the red square is less than in the green one, they are replaced. The red square moves up, and the green one down, and in the future they change places.

Animation scheme

Animation scheme

In Python, this can be described as follows:

left_x = left * cellSize + space * left
right_x = right * cellSize + space * right

left_y = 50
right_y = 50

main_frame = 0

width = (cellSize + space + 2) * len(self.data)
height = cellSize * 6

dx = cellSize
dy = cellSize / 2

font = ImageFont.truetype("arial.ttf", 15)

for main_frame in range(30):
    img = Image.new('RGB', (width, height), (255, 255, 255))
    draw = ImageDraw.Draw(img)

    for i, n in enumerate(self.data):
        if i not in [left, right]:
            draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                            50 - cellSize / 2 + dy,
                            i * cellSize + cellSize / 2 + i * space + dx,
                            50 + cellSize / 2 + dy],
                            fill = None, outline = (0, 0, 0))

            draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                        50 - cellSize / 2 + dy],
                        text = str(n), fill = (0, 0, 0), font = font)

    draw.rectangle([left_x - cellSize / 2 + dx,
                    left_y - cellSize / 2 + dy,
                    left_x + cellSize / 2 + dx,
                    left_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

    draw.text([left_x - cellSize / 2 + dx,
                left_y - cellSize / 2 + dy],
                text = str(self.data[left]),
                fill = (0, 0, 0),
                font = font)

    draw.rectangle([right_x - cellSize / 2 + dx,
                    right_y - cellSize / 2 + dy,
                    right_x + cellSize / 2 + dx,
                    right_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

    draw.text([right_x - cellSize / 2 + dx,
                right_y - cellSize / 2 + dy],
                text = str(self.data[right]),
                fill = (0, 0, 0),
                font = font)

    main_frame += 1

    if main_frame < 10:
        left_y += 5
        right_y -= 5

    elif main_frame < 20:
        left_x -= (left - right) * (cellSize + space) / 10
        right_x += (left - right) * (cellSize + space) / 10

    elif main_frame < 29:
        left_y -= 5
        right_y += 5

    self.frames.append(img)

self.data[left], self.data[right] = self.data[right], self.data[left]

My result

As part of the development, bubble sorting was used:

arr = AnimatedList(size = 10)

for i in range(10):
    arr[i] = randint(-10, 10)

for i in range(10):
    for j in range(9):
        if arr.data[j] > arr.data[j + 1]:
            arr.swap(j, j + 1)
arr.save_animation('animation.gif')

This type of sorting generates gif animation. Gif animation represents the movement of array elements, which is schematically shown in the figure below.

Animation scheme

Animation scheme

Since the animation runs without errors, the library is written correctly. Therefore, the goal set in the project has been achieved! Hurray!

Full code

from random import randint
from PIL import Image, ImageDraw, ImageFont


class AnimatedList:
    def __init__(self, data = [], size = 0):
        self.frames = []

        self.data = [None for i in range(size)]

        if data:
            self.data = data

    def __setitem__(self, index, value):
        self.data[index] = value

        width = (cellSize + space + 2) * len(self.data)
        height = cellSize * 6

        dx = cellSize
        dy = cellSize / 2

        img = Image.new('RGB', (width, height), (255, 255, 255))
        draw = ImageDraw.Draw(img)

        font = ImageFont.truetype("arial.ttf", 15)

        for i, n in enumerate(self.data):
            draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                            50 - cellSize / 2 + dy,
                            i * cellSize + cellSize / 2 + i * space + dx,
                            50 + cellSize / 2 + dy],
                           fill = None, outline = (0, 0, 0))

            if n != None:
                draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                           50 - cellSize / 2 + dy],
                          text = str(n), fill = (0, 0, 0), font = font)

        self.frames.append(img)

    def __getitem__(self, *args):
        pass

    def swap(self, left, right):
        left_x = left * cellSize + space * left
        right_x = right * cellSize + space * right

        left_y = 50
        right_y = 50

        main_frame = 0

        width = (cellSize + space + 2) * len(self.data)
        height = cellSize * 6

        dx = cellSize
        dy = cellSize / 2

        font = ImageFont.truetype("arial.ttf", 15)
        
        for main_frame in range(30):
            img = Image.new('RGB', (width, height), (255, 255, 255))
            draw = ImageDraw.Draw(img)

            for i, n in enumerate(self.data):
                if i not in [left, right]:
                    draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                                    50 - cellSize / 2 + dy,
                                    i * cellSize + cellSize / 2 + i * space + dx,
                                    50 + cellSize / 2 + dy],
                                   fill = None, outline = (0, 0, 0))

                    draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                               50 - cellSize / 2 + dy],
                              text = str(n), fill = (0, 0, 0), font = font)

            draw.rectangle([left_x - cellSize / 2 + dx,
                            left_y - cellSize / 2 + dy,
                            left_x + cellSize / 2 + dx,
                            left_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

            draw.text([left_x - cellSize / 2 + dx,
                       left_y - cellSize / 2 + dy],
                      text = str(self.data[left]),
                      fill = (0, 0, 0),
                      font = font)

            draw.rectangle([right_x - cellSize / 2 + dx,
                            right_y - cellSize / 2 + dy,
                            right_x + cellSize / 2 + dx,
                            right_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

            draw.text([right_x - cellSize / 2 + dx,
                       right_y - cellSize / 2 + dy],
                      text = str(self.data[right]),
                      fill = (0, 0, 0),
                      font = font)

            main_frame += 1

            if main_frame < 10:
                left_y += 5
                right_y -= 5

            elif main_frame < 20:
                left_x -= (left - right) * (cellSize + space) / 10
                right_x += (left - right) * (cellSize + space) / 10

            elif main_frame < 29:
                left_y -= 5
                right_y += 5

            self.frames.append(img)

        self.data[left], self.data[right] = self.data[right], self.data[left]

    def save_animation(self, name):
        self.frames[0].save('/Users/egorkluychikov/Downloads'+name,
                            save_all = True,
                            append_images = self.frames[1:],
                            duration = 100,
                            loop = 0)

cellSize = 20

space = 5

arr = AnimatedList(size = 10)

for i in range(10):
    arr[i] = randint(-10, 10)

for i in range(10):
    for j in range(9):
        if arr.data[j] > arr.data[j + 1]:
            arr.swap(j, j + 1)
arr.save_animation('animation.gif')

List of sources:

  1. What is object-oriented programming (OOP)? – URL: https://itanddigital.ru/oop

  2. What is the difference between an object and a class. – URL: https://uchet-jkh.ru/i/kakaya-raznica-mezdu-obektom-i-klassom – Publication date: August 19, 2023.

  3. What is Self in Python – in detail with examples. – URL: https://python-lab.ru/chto-takoe-self-v-python-podrobno-na-primerah

  4. Pillow (PIL Fork) 10.2.0 documentation. – URL: https://pillow.readthedocs.io/en/stable/index.html

I will be glad to hear your questions and suggestions, because only a prototype has been made and it still needs to be finalized.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *