|
"""
|
|
مولد الرسوم البيانية لنظام إدارة المناقصات
|
|
"""
|
|
|
|
import os
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib as mpl
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
import tkinter as tk
|
|
import customtkinter as ctk
|
|
|
|
class ChartGenerator:
|
|
"""فئة مولد الرسوم البيانية"""
|
|
|
|
def __init__(self, theme):
|
|
"""تهيئة مولد الرسوم البيانية"""
|
|
self.theme = theme
|
|
|
|
|
|
self.charts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "charts")
|
|
|
|
|
|
os.makedirs(self.charts_dir, exist_ok=True)
|
|
|
|
|
|
self._setup_chart_style()
|
|
|
|
def _setup_chart_style(self):
|
|
"""إعداد نمط الرسوم البيانية"""
|
|
|
|
plt.style.use('ggplot')
|
|
|
|
|
|
plt.rcParams['font.family'] = 'sans-serif'
|
|
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans', 'Bitstream Vera Sans', 'sans-serif']
|
|
|
|
|
|
plt.rcParams['font.size'] = 10
|
|
plt.rcParams['axes.titlesize'] = 14
|
|
plt.rcParams['axes.labelsize'] = 12
|
|
plt.rcParams['xtick.labelsize'] = 10
|
|
plt.rcParams['ytick.labelsize'] = 10
|
|
plt.rcParams['legend.fontsize'] = 10
|
|
|
|
|
|
if self.theme.current_theme == "light":
|
|
plt.rcParams['figure.facecolor'] = self.theme.LIGHT_CARD_BG_COLOR
|
|
plt.rcParams['axes.facecolor'] = self.theme.LIGHT_BG_COLOR
|
|
plt.rcParams['axes.edgecolor'] = self.theme.LIGHT_BORDER_COLOR
|
|
plt.rcParams['axes.labelcolor'] = self.theme.LIGHT_FG_COLOR
|
|
plt.rcParams['xtick.color'] = self.theme.LIGHT_FG_COLOR
|
|
plt.rcParams['ytick.color'] = self.theme.LIGHT_FG_COLOR
|
|
plt.rcParams['text.color'] = self.theme.LIGHT_FG_COLOR
|
|
plt.rcParams['grid.color'] = self.theme.LIGHT_BORDER_COLOR
|
|
else:
|
|
plt.rcParams['figure.facecolor'] = self.theme.DARK_CARD_BG_COLOR
|
|
plt.rcParams['axes.facecolor'] = self.theme.DARK_BG_COLOR
|
|
plt.rcParams['axes.edgecolor'] = self.theme.DARK_BORDER_COLOR
|
|
plt.rcParams['axes.labelcolor'] = self.theme.DARK_FG_COLOR
|
|
plt.rcParams['xtick.color'] = self.theme.DARK_FG_COLOR
|
|
plt.rcParams['ytick.color'] = self.theme.DARK_FG_COLOR
|
|
plt.rcParams['text.color'] = self.theme.DARK_FG_COLOR
|
|
plt.rcParams['grid.color'] = self.theme.DARK_BORDER_COLOR
|
|
|
|
def create_bar_chart(self, data, title, xlabel, ylabel):
|
|
"""إنشاء رسم بياني شريطي"""
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
|
|
|
|
|
bars = ax.bar(data['labels'], data['values'], color=self.theme.PRIMARY_COLOR[self.theme.current_theme])
|
|
|
|
|
|
for bar in bars:
|
|
height = bar.get_height()
|
|
ax.text(bar.get_x() + bar.get_width() / 2., height + 0.1 * max(data['values']),
|
|
f'{height:,.0f}', ha='center', va='bottom')
|
|
|
|
|
|
ax.set_title(title)
|
|
ax.set_xlabel(xlabel)
|
|
ax.set_ylabel(ylabel)
|
|
|
|
|
|
ax.set_ylim(0, max(data['values']) * 1.2)
|
|
|
|
|
|
ax.grid(True, linestyle='--', alpha=0.7)
|
|
|
|
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
def create_line_chart(self, data, title, xlabel, ylabel):
|
|
"""إنشاء رسم بياني خطي"""
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
|
|
|
|
|
line = ax.plot(data['labels'], data['values'], marker='o', linestyle='-', linewidth=2,
|
|
color=self.theme.PRIMARY_COLOR[self.theme.current_theme],
|
|
markersize=8, markerfacecolor=self.theme.SECONDARY_COLOR[self.theme.current_theme])
|
|
|
|
|
|
for i, value in enumerate(data['values']):
|
|
ax.text(i, value + 0.05 * max(data['values']), f'{value:,.0f}', ha='center', va='bottom')
|
|
|
|
|
|
ax.set_title(title)
|
|
ax.set_xlabel(xlabel)
|
|
ax.set_ylabel(ylabel)
|
|
|
|
|
|
ax.set_ylim(0, max(data['values']) * 1.2)
|
|
|
|
|
|
ax.grid(True, linestyle='--', alpha=0.7)
|
|
|
|
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
def create_pie_chart(self, data, title):
|
|
"""إنشاء رسم بياني دائري"""
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
|
|
|
|
|
colors = [
|
|
self.theme.PRIMARY_COLOR[self.theme.current_theme],
|
|
self.theme.SECONDARY_COLOR[self.theme.current_theme],
|
|
self.theme.ACCENT_COLOR[self.theme.current_theme],
|
|
self.theme.WARNING_COLOR[self.theme.current_theme],
|
|
self.theme.SUCCESS_COLOR[self.theme.current_theme]
|
|
]
|
|
|
|
|
|
wedges, texts, autotexts = ax.pie(
|
|
data['values'],
|
|
labels=data['labels'],
|
|
autopct='%1.1f%%',
|
|
startangle=90,
|
|
colors=colors,
|
|
wedgeprops={'edgecolor': 'white', 'linewidth': 1},
|
|
textprops={'color': self.theme.get_color('fg_color')}
|
|
)
|
|
|
|
|
|
for autotext in autotexts:
|
|
autotext.set_color('white')
|
|
autotext.set_fontweight('bold')
|
|
|
|
|
|
ax.set_title(title)
|
|
|
|
|
|
ax.axis('equal')
|
|
|
|
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
def create_stacked_bar_chart(self, data, title, xlabel, ylabel):
|
|
"""إنشاء رسم بياني شريطي متراكم"""
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
|
|
|
|
|
colors = [
|
|
self.theme.PRIMARY_COLOR[self.theme.current_theme],
|
|
self.theme.SECONDARY_COLOR[self.theme.current_theme],
|
|
self.theme.ACCENT_COLOR[self.theme.current_theme],
|
|
self.theme.WARNING_COLOR[self.theme.current_theme],
|
|
self.theme.SUCCESS_COLOR[self.theme.current_theme]
|
|
]
|
|
|
|
|
|
bottom = np.zeros(len(data['labels']))
|
|
for i, category in enumerate(data['categories']):
|
|
values = data['values'][i]
|
|
bars = ax.bar(data['labels'], values, bottom=bottom, label=category, color=colors[i % len(colors)])
|
|
bottom += values
|
|
|
|
|
|
ax.set_title(title)
|
|
ax.set_xlabel(xlabel)
|
|
ax.set_ylabel(ylabel)
|
|
|
|
|
|
ax.legend()
|
|
|
|
|
|
ax.grid(True, linestyle='--', alpha=0.7)
|
|
|
|
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
def create_risk_matrix(self, data, title):
|
|
"""إنشاء مصفوفة المخاطر"""
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
|
|
|
|
|
colors = {
|
|
'منخفض': self.theme.SUCCESS_COLOR[self.theme.current_theme],
|
|
'متوسط': self.theme.WARNING_COLOR[self.theme.current_theme],
|
|
'عالي': self.theme.ERROR_COLOR[self.theme.current_theme]
|
|
}
|
|
|
|
|
|
probability_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
|
|
impact_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
|
|
|
|
|
|
for risk in data['risks']:
|
|
prob = probability_values[risk['probability']]
|
|
impact = impact_values[risk['impact']]
|
|
color = colors[risk['probability']] if prob > impact else colors[risk['impact']]
|
|
ax.scatter(impact, prob, color=color, s=100, alpha=0.7)
|
|
ax.annotate(risk['name'], (impact, prob), xytext=(5, 5), textcoords='offset points')
|
|
|
|
|
|
ax.set_xlim(0.5, 3.5)
|
|
ax.set_ylim(0.5, 3.5)
|
|
|
|
|
|
ax.set_xticks([1, 2, 3])
|
|
ax.set_xticklabels(['منخفض', 'متوسط', 'عالي'])
|
|
ax.set_yticks([1, 2, 3])
|
|
ax.set_yticklabels(['منخفض', 'متوسط', 'عالي'])
|
|
|
|
|
|
ax.set_title(title)
|
|
ax.set_xlabel('التأثير')
|
|
ax.set_ylabel('الاحتمالية')
|
|
|
|
|
|
ax.grid(True, linestyle='--', alpha=0.7)
|
|
|
|
|
|
|
|
ax.add_patch(plt.Rectangle((0.5, 0.5), 1, 1, fill=True, color=self.theme.SUCCESS_COLOR[self.theme.current_theme], alpha=0.1))
|
|
|
|
ax.add_patch(plt.Rectangle((1.5, 0.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
|
|
ax.add_patch(plt.Rectangle((0.5, 1.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
|
|
|
|
ax.add_patch(plt.Rectangle((2.5, 0.5), 1, 3, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
|
|
ax.add_patch(plt.Rectangle((0.5, 2.5), 2, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
|
|
ax.add_patch(plt.Rectangle((1.5, 1.5), 1, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
|
|
|
|
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
def embed_chart_in_frame(self, parent, fig):
|
|
"""تضمين الرسم البياني في إطار"""
|
|
|
|
chart_frame = ctk.CTkFrame(parent, fg_color="transparent")
|
|
|
|
|
|
canvas = FigureCanvasTkAgg(fig, master=chart_frame)
|
|
canvas.draw()
|
|
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
|
|
|
return chart_frame
|
|
|
|
def save_chart(self, fig, name):
|
|
"""حفظ الرسم البياني"""
|
|
|
|
file_path = os.path.join(self.charts_dir, f"{name}.png")
|
|
|
|
|
|
fig.savefig(file_path, dpi=100, bbox_inches='tight')
|
|
|
|
return file_path
|
|
|