отрисовка схемы по условиям из excel (python)
Разрабатываю программу для автоматизации создания чертежей-схем на производстве. Если кратко:
- загрузка excel таблицы со списком устройств и их характеристиками.
- Создание дополнительных таблиц по условию.
- Создание чертежа по шаблону и таблицам.
В чем проблема? Мне нужно добавлять в этот чертеж 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 шт):
Для ответа на Ваш вопрос надо понимать:
- структуру и типизацию входных данных;
- данные в стандартизированных таблицах или с некоторой вариативностью;
- можете ли Вы влиять на формат, или принимаете как есть.
Понимая это можно работать над алгоритмом парсинга и создания объектов.
В дополнение, вариант парсинга таблиц (альтернатива словарю)
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.