отрисовка схемы по условиям из excel (python)

Разрабатываю программу для автоматизации создания чертежей-схем на производстве. Если кратко:

  1. загрузка excel таблицы со списком устройств и их характеристиками.
  2. Создание дополнительных таблиц по условию.
  3. Создание чертежа по шаблону и таблицам.

В чем проблема? Мне нужно добавлять в этот чертеж svg-шки по названию и условиям из таблиц, учитывая их нахождение в новых табличках, вписывать на них комментарии. Использую matplotlib для создания схем (разбил на 14х5 изображение и подгружаю по координатам изображения как шаблоны), не понимаю как реализовать написанное выше. Интересуют подобные проекты с похожей реализацией или идеи как мне передавать данные и работать с ними шаблонами. Пример кода ниже:

import tkinter as tk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.patches import Rectangle
import os


# Размер A3 в миллиметрах
a3_width_mm, a3_height_mm = 420, 297  # размеры в мм

# Размеры в дюймах
a3_width_inch, a3_height_inch = a3_width_mm / 25.4, a3_height_mm / 25.4  # размеры в дюймах

# DPI (точек на дюйм)
dpi = 300  # Высокое разрешение

# Размеры в пикселях
a3_width_px = int(a3_width_inch * dpi)
a3_height_px = int(a3_height_inch * dpi)

# Количество столбцов и рядов
num_columns = 14
num_rows = 5

# Вычисляем ширину и высоту каждого блока
column_width = a3_width_px / num_columns
row_height = a3_height_px / num_rows

# Шаблоны для ячеек с указанием изображений и текста с координатами
templates = {
   
}

# Переменная для отслеживания состояния сетки
show_grid = True

def draw_shapes(ax, templates, image_folder):
    ax.clear()
    # Отрисовка шаблонов
    for (i, j), template in templates.items():
        x = i * column_width
        y = a3_height_px - (j + 1) * row_height
        image_path = os.path.join(image_folder, template['image'])
        img = plt.imread(image_path)
        ax.imshow(img, extent=[x, x + column_width, y, y + row_height])

        # Добавление текста, если он есть в шаблоне
        if 'text' in template and 'text_coords' in template:
            text = template['text']
            text_x, text_y = template['text_coords']
            ax.text(text_x, text_y, text, ha='center', va='center', fontsize=7, color='black')

    # Добавление линий сетки, если нужно
    if show_grid:
        for i in range(num_columns + 1):
            x = i * column_width
            ax.plot([x, x], [0, a3_height_px], color='black', linewidth=0.1)  # Линии сетки 0.1 мм

        for j in range(num_rows + 1):
            y = j * row_height
            ax.plot([0, a3_width_px], [y, y], color='black', linewidth=0.1)  # Линии сетки 0.1 мм

    # Установка границ осей
    ax.set_xlim(0, a3_width_px)
    ax.set_ylim(0, a3_height_px)

    # Отключение сетки и осей
    ax.axis('off')

    # Добавление рамки вокруг всей схемы с отступом 5 мм
    offset_mm = 5  # Отступ в миллиметрах
    offset_px = offset_mm * dpi / 25.4  # Конвертация отступа в пиксели

    rect = Rectangle((offset_px, offset_px), a3_width_px - 2 * offset_px, a3_height_px - 2 * offset_px, linewidth=0.5, edgecolor='black', facecolor='none')
    ax.add_patch(rect)

    # Добавление изображения-шаблона, занимающего 4 клетки по горизонтали и 1 клетку по вертикали
    x_image = 10 * column_width  # начало изображения с 10-го столбца
    y_image = a3_height_px - 5 * row_height  # начало изображения с 2-го ряда
    image_path_template = os.path.join(image_folder, 'info_niz.png')
    img_template = plt.imread(image_path_template)
    ax.imshow(img_template, extent=[x_image, x_image + 4 * column_width, y_image, y_image + row_height])

    # Добавление изображения-шаблона на левой стороне, занимающего 5 клеток по вертикали и 1 клетку по горизонтали
    x_image = 0  # начало изображения с левого края
    y_image = a3_height_px - 5 * row_height  # начало изображения с 1-го ряда сверху
    image_path_template = os.path.join(image_folder, 'left.png')  # путь к изображению-шаблону
    img_template = plt.imread(image_path_template)
    ax.imshow(img_template, extent=[x_image, x_image + column_width, y_image, y_image + 5 * row_height])

def toggle_grid():
    global show_grid
    show_grid = not show_grid
    draw_shapes(ax, templates, image_folder)
    canvas.draw()

def save_image():
    file_path = os.path.join(script_dir, 'output.svg')
    fig.set_size_inches(a3_width_inch, a3_height_inch)  # Устанавливаем размеры перед сохранением
    fig.savefig(file_path, format='svg')  # Сохраняем в формате SVG
    print(f"Saved image to {file_path}")

def plot_graph():
    global fig, ax, canvas, script_dir, image_folder

    rect_window = tk.Tk()
    rect_window.attributes('-fullscreen', False)  # Полноэкранный режим

    # Frame для кнопок управления
    button_frame = tk.Frame(rect_window)
    button_frame.pack(side=tk.BOTTOM, fill=tk.X)

    # Кнопка для переключения сетки
    toggle_grid_button = tk.Button(button_frame, text="Toggle Grid", command=toggle_grid)
    toggle_grid_button.pack(side=tk.LEFT, padx=10, pady=10)

    # Кнопка для сохранения изображения
    save_image_button = tk.Button(button_frame, text="Save Image", command=save_image)
    save_image_button.pack(side=tk.LEFT, padx=10, pady=10)

    # Frame для отображения чертежа
    drawing_frame = tk.Frame(rect_window)
    drawing_frame.pack(fill=tk.BOTH, expand=True)

    fig, ax = plt.subplots(figsize=(a3_width_inch, a3_height_inch), dpi=dpi)

    script_dir = os.path.dirname(os.path.abspath(__file__))
    image_folder = os.path.join(script_dir, 'data')  # Указание папки data для изображений

    draw_shapes(ax, templates, image_folder)

    canvas = FigureCanvasTkAgg(fig, master=drawing_frame)
    canvas.draw()
    canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

    rect_window.mainloop()

if __name__ == "__main__":
    plot_graph()

Ответы (1 шт):

Автор решения: AnnaBazueva

Для ответа на Ваш вопрос надо понимать:

  • структуру и типизацию входных данных;
  • данные в стандартизированных таблицах или с некоторой вариативностью;
  • можете ли Вы влиять на формат, или принимаете как есть.

Понимая это можно работать над алгоритмом парсинга и создания объектов.

В дополнение, вариант парсинга таблиц (альтернатива словарю)

from collections import namedtuple

from rich import inspect
import pylightxl as xl


_trans_args = (
    '_type',
    'brand',
    'model',
    'color',
    'reg_num',
    'vin_num',
    'length',
    'width',
    'height',
    'weight',
    'cargo',
    'cargo_name',
    'quantity',
    'cargo_weight',
    'hazard_class',
    'un_num',
    'tn_ved_nums'
    )
_state = (
    'surname',
    'name',
    'patronymic',
    'phone',
    'e_mail',
    'number',
    'date',
    'organization',
    'birthdate',
    'place_birth',
    'registration',
    'series',
    'inn',
    'position'
    )

Transport = namedtuple('Transport', _trans_args)
Сitizen = namedtuple('Сitizen', _state[:12])
NonCitizen = namedtuple('NonCitizen', _state[:11])
LegalPerson = namedtuple('LegalPerson', _state[:5]+_state[-2:])
Actor = namedtuple('Actor', ['_type', 'state'])
Task = namedtuple('Task', ['flight', 'transports', 'actors'])
Tasks = namedtuple(
    'Tasks', ['task_1', 'task_2', 'task_3'], defaults=(None, None,)
    )

db = xl.readxl(fn='Данные заявки.xlsx', ws=('task_1', 'task_2', 'task_3'))

_tasks = []
for sheet in db.ws_names:
    if not (fl := db.ws(sheet).address(address='B1')):
        continue
    ssd = db.ws(sheet).ssd(keycols="KEYCOLS", keyrows="KEYROWS")
    _transports = []
    _actors = []
    for i, table in enumerate(ssd):
        for k, col in enumerate(table.get('keycols')[:-1]):
            if (any(ob := tuple(row[k] for row in table.get('data')
                                if row[-1] in _trans_args or row[k]))
                    and (len(_trans_args) == len(ob)
                         or (_citizen := len(ob) == 12)
                         or (_non_citizen := len(ob) == 11)
                         or (_legal_person := len(ob) == 7))):
                _keys = table.get('keyrows')
                if len(_keys) == len(ob):
                    keys = _keys
                elif _citizen:
                    keys = _keys[:12]
                elif _non_citizen:
                    keys = _keys[:11]
                elif _legal_person:
                    keys = _keys[:5]+_keys[-2:]
                else:
                    print("Error")

                _t = {key: val for key, val in zip(keys, ob)}
                if i:
                    if _citizen:
                        state = Сitizen(**_t)
                    elif _non_citizen:
                        state = NonCitizen(**_t)
                    else:
                        state = LegalPerson(**_t)
                    _actors.append(Actor(col, state))
                else:
                    _transports.append(Transport(**_t))
    else:
        _tasks.append(
            Task(flight=fl, transports=tuple(_transports), actors=tuple(_actors))
            )
else:
    tasks = Tasks(*_tasks)
    inspect(tasks, value=True)

С именованными кортежами работать удобнее чем со словарём,
в Вашем случае однозначно удобнее т.к. данные не меняются.
Тестовый файл посмотрите какую структуру данных можно получить.

╭─────────────────── <class '__main__.Tasks'> ────────────────────╮
│ Tasks(task_1, task_2, task_3)                                   │
│                                                                 │
│ ╭─────────────────────────────────────────────────────────────╮ │
│ │ Tasks(                                                      │ │
│ │ │   task_1=Task(                                            │ │
│ │ │   │   flight='GCH00219UB',                                │ │
│ │ │   │   transports=(                                        │ │
│ │ │   │   │   Transport(                                      │ │
│ │ │   │   │   │   _type='CAR',                                │ │
│ │ │   │   │   │   brand='LADA',                               │ │
│ │ │   │   │   │   model='Largus',                             │ │
│ │ │   │   │   │   color='Серый металлик',                     │ │
│ │ │   │   │   │   reg_num='У967ВР18',                         │ │
│ │ │   │   │   │   vin_num='',                                 │ │
│ │ │   │   │   │   length='4,5',                               │ │
│ │ │   │   │   │   width='1,8',                                │ │
│ │ │   │   │   │   height='1,6',                               │ │
│ │ │   │   │   │   weight='1260',                              │ │
│ │ │   │   │   │   cargo='',                                   │ │
│ │ │   │   │   │   cargo_name='',                              │ │
│ │ │   │   │   │   quantity='',                                │ │
│ │ │   │   │   │   cargo_weight='',                            │ │
│ │ │   │   │   │   hazard_class='',                            │ │
│ │ │   │   │   │   un_num='',                                  │ │
│ │ │   │   │   │   tn_ved_nums=''                              │ │
│ │ │   │   │   ),                                              │ │
│ │ │   │   │   Transport(                                      │ │
│ │ │   │   │   │   _type='CAR',                                │ │
│ │ │   │   │   │   brand='Renault',                            │ │
│ │ │   │   │   │   model='Captur',                             │ │
│ │ │   │   │   │   color='Красный',                            │ │
│ │ │   │   │   │   reg_num='',                                 │ │
│ │ │   │   │   │   vin_num='WAUZZZ8AZMA123456',                │ │
│ │ │   │   │   │   length='4,1',                               │ │
│ │ │   │   │   │   width='1,8',                                │ │
│ │ │   │   │   │   height='1,6',                               │ │
│ │ │   │   │   │   weight='1184',                              │ │
│ │ │   │   │   │   cargo='',                                   │ │
│ │ │   │   │   │   cargo_name='',                              │ │
│ │ │   │   │   │   quantity='',                                │ │
│ │ │   │   │   │   cargo_weight='',                            │ │
│ │ │   │   │   │   hazard_class='',                            │ │
│ │ │   │   │   │   un_num='',                                  │ │
│ │ │   │   │   │   tn_ved_nums=''                              │ │
│ │ │   │   │   ),                                              │ │
│ │ │   │   │   Transport(                                      │ │
│ │ │   │   │   │   _type='TR',                                 │ │
│ │ │   │   │   │   brand='Тактика',                            │ │
│ │ │   │   │   │   model='300Б - бортовой прицеп',             │ │
│ │ │   │   │   │   color='Оцинкованный',                       │ │
│ │ │   │   │   │   reg_num='',                                 │ │
│ │ │   │   │   │   vin_num='X4381771DG0047559',                │ │
│ │ │   │   │   │   length='3,5',                               │ │
│ │ │   │   │   │   width='2',                                  │ │
│ │ │   │   │   │   height='1,2',                               │ │
│ │ │   │   │   │   weight='200',                               │ │
│ │ │   │   │   │   cargo='',                                   │ │
│ │ │   │   │   │   cargo_name='',                              │ │
│ │ │   │   │   │   quantity='',                                │ │
│ │ │   │   │   │   cargo_weight='',                            │ │
│ │ │   │   │   │   hazard_class='',                            │ │
│ │ │   │   │   │   un_num='',                                  │ │
│ │ │   │   │   │   tn_ved_nums=''                              │ │
│ │ │   │   │   )                                               │ │
│ │ │   │   ),                                                  │ │
│ │ │   │   actors=(                                            │ │
│ │ │   │   │   Actor(                                          │ │
│ │ │   │   │   │   _type='Payer',                              │ │
│ │ │   │   │   │   state=Сitizen(                              │ │
│ │ │   │   │   │   │   surname='Иванов',                       │ │
│ │ │   │   │   │   │   name='Иван',                            │ │
│ │ │   │   │   │   │   patronymic='Иванович',                  │ │
│ │ │   │   │   │   │   phone='+79113171307',                   │ │
│ │ │   │   │   │   │   e_mail='[email protected]',                  │ │
│ │ │   │   │   │   │   number='715312',                        │ │
│ │ │   │   │   │   │   date='06.05.2018',                      │ │
│ │ │   │   │   │   │   organization='МВД по Ивановской обл.',  │ │
│ │ │   │   │   │   │   birthdate='11.02.1982',                 │ │
│ │ │   │   │   │   │   place_birth='гор. Иваново',             │ │
│ │ │   │   │   │   │   registration='Не дом и не улица',       │ │
│ │ │   │   │   │   │   series='9418'                           │ │
│ │ │   │   │   │   )                                           │ │
│ │ │   │   │   ),                                              │ │
│ │ │   │   │   Actor(                                          │ │
│ │ │   │   │   │   _type='Sender',                             │ │
│ │ │   │   │   │   state=LegalPerson(                          │ │
│ │ │   │   │   │   │   surname='Лаптева',                      │ │
│ │ │   │   │   │   │   name='Юлия',                            │ │
│ │ │   │   │   │   │   patronymic='Николаевна',                │ │
│ │ │   │   │   │   │   phone='8 (812) 448-88-88',              │ │
│ │ │   │   │   │   │   e_mail='[email protected]',    │ │
│ │ │   │   │   │   │   inn='7826156685',                       │ │
│ │ │   │   │   │   │   position='Менеджер'                     │ │
│ │ │   │   │   │   )                                           │ │
│ │ │   │   │   )                                               │ │
│ │ │   │   )                                                   │ │
│ │ │   ),                                                      │ │
│ │ │   task_2=None,                                            │ │
│ │ │   task_3=None                                             │ │
│ │ )                                                           │ │
│ ╰─────────────────────────────────────────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────╯

Т.е. при парсинге из файла.xlsx надо структурировать данные таким образом,
чтобы один в дальнейшем использовался Tkinter a другой matplotlib.

→ Ссылка