Update app/simulation/visualization/animation.py
Browse files
app/simulation/visualization/animation.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
-
|
| 5 |
|
| 6 |
def minutes_to_time(minutes, start_time="00:00"):
|
| 7 |
start_hour, start_min = map(int, start_time.split(':'))
|
|
@@ -10,148 +12,6 @@ def minutes_to_time(minutes, start_time="00:00"):
|
|
| 10 |
minute = total_minutes % 60
|
| 11 |
return f"{hour:02d}:{minute:02d}"
|
| 12 |
|
| 13 |
-
|
| 14 |
-
def create_animation_frame_plotly(frame_data, specialists_count, second_model_name="XGBoost"):
|
| 15 |
-
# Фиксированная ось X для графиков
|
| 16 |
-
time_ticks = list(range(0, 1441, 180))
|
| 17 |
-
time_labels = [minutes_to_time(t, "00:00") for t in time_ticks]
|
| 18 |
-
|
| 19 |
-
fig = make_subplots(
|
| 20 |
-
rows=3, cols=2,
|
| 21 |
-
subplot_titles=('📈 Динамика входящего потока', '⚙️ Загрузка специалистов (%)',
|
| 22 |
-
'👥 МОНИТОРИНГ РАБОТЫ СПЕЦИАЛИСТОВ', '',
|
| 23 |
-
'📊 Сводная статистика обработки', '🎯 Оперативные показатели'),
|
| 24 |
-
specs=[
|
| 25 |
-
[{'type': 'scatter'}, {'type': 'scatter'}],
|
| 26 |
-
[{'type': 'heatmap', 'colspan': 2}, None],
|
| 27 |
-
[{'type': 'table'}, {'type': 'scatter'}]
|
| 28 |
-
],
|
| 29 |
-
row_heights=[0.25, 0.40, 0.35],
|
| 30 |
-
vertical_spacing=0.1,
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
# --- РЯД 1: ГРАФИКИ ---
|
| 34 |
-
inflow_h = frame_data.get('inflow_history', [])
|
| 35 |
-
load_h = frame_data.get('load_history', [])
|
| 36 |
-
|
| 37 |
-
fig.add_trace(go.Scatter(y=inflow_h, fill='tozeroy', line=dict(color='#4361ee', width=2)), row=1, col=1)
|
| 38 |
-
fig.add_trace(go.Scatter(y=[l * 100 for l in load_h], fill='tozeroy', line=dict(color='#4cc9f0', width=2)), row=1,
|
| 39 |
-
col=2)
|
| 40 |
-
|
| 41 |
-
for col in [1, 2]:
|
| 42 |
-
fig.update_xaxes(range=[0, 1440], tickvals=time_ticks, ticktext=time_labels, row=1, col=col)
|
| 43 |
-
fig.update_yaxes(rangemode="tozero", row=1, col=col)
|
| 44 |
-
|
| 45 |
-
# --- РЯД 2: HEATMAP (Строго 20 ячеек в ширину) ---
|
| 46 |
-
states = np.array(frame_data['specialist_states'])
|
| 47 |
-
cols = 20
|
| 48 |
-
rows = int(np.ceil(specialists_count / cols))
|
| 49 |
-
|
| 50 |
-
# Создаем матрицу, заполненную None (или NaN), чтобы пустые места не красились
|
| 51 |
-
z_matrix = np.full((rows, cols), np.nan)
|
| 52 |
-
for i, val in enumerate(states):
|
| 53 |
-
r, c = divmod(i, cols)
|
| 54 |
-
# Мапим значения: 0 -> 0.1 (голубой), 1-3 -> 0.4 (зеленый) и т.д.
|
| 55 |
-
if val == 0:
|
| 56 |
-
z_matrix[r, c] = 0.1
|
| 57 |
-
elif val <= 3:
|
| 58 |
-
z_matrix[r, c] = 0.4
|
| 59 |
-
elif val <= 7:
|
| 60 |
-
z_matrix[r, c] = 0.7
|
| 61 |
-
else:
|
| 62 |
-
z_matrix[r, c] = 1.0
|
| 63 |
-
|
| 64 |
-
# Настраиваем цвета: NaN будет прозрачным/фоновым
|
| 65 |
-
colorscale = [
|
| 66 |
-
[0.0, '#66ccff'], # Свободен (0)
|
| 67 |
-
[0.4, '#4ade80'], # 1-3 мин
|
| 68 |
-
[0.7, '#facc15'], # 4-7 мин
|
| 69 |
-
[1.0, '#f87171'] # 8+ мин
|
| 70 |
-
]
|
| 71 |
-
|
| 72 |
-
fig.add_trace(go.Heatmap(
|
| 73 |
-
z=z_matrix, colorscale=colorscale, showscale=False,
|
| 74 |
-
xgap=2, ygap=2, zmin=0, zmax=1, hoverinfo='none'
|
| 75 |
-
), row=2, col=1)
|
| 76 |
-
|
| 77 |
-
# Легенда над хитмапом
|
| 78 |
-
free = sum(1 for t in states if t <= 0)
|
| 79 |
-
legend = (f"Свободно: <b>{free}</b> | <span style='color:#66ccff'>■</span> Свободен "
|
| 80 |
-
f"<span style='color:#4ade80'>■</span> 1-3м <span style='color:#facc15'>■</span> 4-7м "
|
| 81 |
-
f"<span style='color:#f87171'>■</span> 8м+")
|
| 82 |
-
fig.add_annotation(text=legend, xref="paper", yref="paper", x=0.5, y=0.70, showarrow=False, font=dict(size=14))
|
| 83 |
-
|
| 84 |
-
# --- РЯД 3: ТАБЛИЦА (Формальная) ---
|
| 85 |
-
cum = frame_data['cumulative']
|
| 86 |
-
fig.add_trace(go.Table(
|
| 87 |
-
header=dict(values=['Параметр', 'Значение'], fill_color='#1e293b', font=dict(color='white', size=15),
|
| 88 |
-
height=35),
|
| 89 |
-
cells=dict(values=[
|
| 90 |
-
['✅ Авто-одобрено', '❌ Авто-отказы', '👤 На рассмотрении (Manual)', '<b>ИТОГО ОБРАБОТАНО</b>'],
|
| 91 |
-
[cum['auto_approved'], cum['auto_declined'],
|
| 92 |
-
cum['manual_processed'] + cum['business_manual_processed'], f"<b>{cum['total_processed']}</b>"]
|
| 93 |
-
], align='left', font=dict(size=14), height=35, fill_color='#f8f9fa')
|
| 94 |
-
), row=3, col=1)
|
| 95 |
-
|
| 96 |
-
# --- ОПЕРАТИВНЫЕ ПОКАЗАТЕЛИ (Крупный заголовок) ---
|
| 97 |
-
q_models = frame_data['queue'] # Очередь к спецам
|
| 98 |
-
q_business = frame_data.get('business_queue', 0) # Бизнес-очередь
|
| 99 |
-
|
| 100 |
-
# Расчет ожидания только для очереди моделей (как на левом графике)
|
| 101 |
-
avg_w = frame_data.get('avg_wait', 0)
|
| 102 |
-
|
| 103 |
-
status_card = (
|
| 104 |
-
f"<span style='font-size:22px; font-weight:bold;'>МОНИТОРИН��</span><br><br>"
|
| 105 |
-
f"<span style='background-color:#dcfce7; color:#166534; padding:8px; border-radius:5px;'>"
|
| 106 |
-
f"<b>👤 ОЧЕРЕДЬ (СПЕЦ): {q_models}</b></span><br><br>"
|
| 107 |
-
f"<span style='font-size:18px; color:#666;'>"
|
| 108 |
-
f"⚙️ Бизнес-правила: {q_business}</span><br><br>"
|
| 109 |
-
f"🕒 Время: <b>{frame_data['time_str']}</b><br>"
|
| 110 |
-
f"⏳ Ожидание: <b>{avg_w:.1f} мин</b>"
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
fig.add_trace(go.Scatter(x=[0], y=[0], mode='text', text=[status_card], textfont=dict(size=16)), row=3, col=2)
|
| 114 |
-
|
| 115 |
-
# Очистка осей
|
| 116 |
-
fig.update_xaxes(visible=False, row=2, col=1);
|
| 117 |
-
fig.update_yaxes(visible=False, row=2, col=1)
|
| 118 |
-
fig.update_xaxes(visible=False, row=3, col=2);
|
| 119 |
-
fig.update_yaxes(visible=False, row=3, col=2)
|
| 120 |
-
|
| 121 |
-
# Фиксируем оси, чтобы график не "дышал" (это главная причина мерцания)
|
| 122 |
-
fig.update_yaxes(range=[0, 60], row=1, col=1) # Замени 60 на твой макс. поток
|
| 123 |
-
fig.update_yaxes(range=[0, 105], row=1, col=2) # Загрузка всегда до 100%
|
| 124 |
-
|
| 125 |
-
fig.update_layout(
|
| 126 |
-
height=950,
|
| 127 |
-
margin=dict(t=80, b=40, l=50, r=50),
|
| 128 |
-
template="plotly_white",
|
| 129 |
-
showlegend=False,
|
| 130 |
-
# ОТКЛЮЧАЕМ анимации переходов, которые создают эффект мигания
|
| 131 |
-
transition_duration=0,
|
| 132 |
-
hovermode=False
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
# Это заставит Plotly обновлять только данные, не перерисовывая всё полотно
|
| 136 |
-
fig.layout.datarevision = frame_data['time']
|
| 137 |
-
return fig
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
from matplotlib.animation import FFMpegWriter
|
| 141 |
-
|
| 142 |
-
import matplotlib.pyplot as plt
|
| 143 |
-
import matplotlib.animation as animation
|
| 144 |
-
import tempfile
|
| 145 |
-
import numpy as np
|
| 146 |
-
|
| 147 |
-
import matplotlib.pyplot as plt
|
| 148 |
-
import matplotlib.animation as animation
|
| 149 |
-
import tempfile
|
| 150 |
-
import numpy as np
|
| 151 |
-
import os
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
# Внести изменения в функцию create_simulation_video в animation.py
|
| 155 |
def create_simulation_video(frames, specialists_count, second_model_name, fps=24):
|
| 156 |
if not frames:
|
| 157 |
return None
|
|
@@ -185,7 +45,7 @@ def create_simulation_video(frames, specialists_count, second_model_name, fps=24
|
|
| 185 |
axes[0, 1].set_ylim(0, 110)
|
| 186 |
axes[0, 1].set_title(f"ЗАГРУЖЕННОСТЬ СПЕЦИАЛИСТОВ %: {y_load[-1]:.1f}%", fontsize=12, fontweight='bold')
|
| 187 |
|
| 188 |
-
# 3. HEATMAP И ЛЕГЕНДА
|
| 189 |
states = np.array(data['specialist_states'])
|
| 190 |
cols = 20
|
| 191 |
rows = int(np.ceil(specialists_count / cols))
|
|
@@ -210,7 +70,6 @@ def create_simulation_video(frames, specialists_count, second_model_name, fps=24
|
|
| 210 |
q_mod_color = '#991b1b' if data['queue'] > 50 else '#166534'
|
| 211 |
q_biz_color = '#991b1b' if data.get('business_queue', 0) > 50 else '#1e293b'
|
| 212 |
|
| 213 |
-
# Две надписи очередей сверху
|
| 214 |
ax_stat.text(0.25, 0.9, "ОЧЕРЕДЬ\n(МОДЕЛИ)", fontsize=10, ha='center', fontweight='bold')
|
| 215 |
ax_stat.text(0.25, 0.78, f"{data['queue']}", fontsize=26, ha='center', fontweight='bold', color=q_mod_color)
|
| 216 |
|
|
@@ -218,7 +77,7 @@ def create_simulation_video(frames, specialists_count, second_model_name, fps=24
|
|
| 218 |
ax_stat.text(0.75, 0.78, f"{data.get('business_queue', 0)}", fontsize=26, ha='center', fontweight='bold',
|
| 219 |
color=q_biz_color)
|
| 220 |
|
| 221 |
-
# Сводная таблица
|
| 222 |
cum = data['cumulative']
|
| 223 |
stats_text = (
|
| 224 |
f"Итоговые показатели к {data['time_str']}\n"
|
|
|
|
| 1 |
+
from matplotlib.animation import FFMpegWriter
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.animation as animation
|
| 4 |
+
import tempfile
|
| 5 |
import numpy as np
|
| 6 |
+
import os
|
| 7 |
|
| 8 |
def minutes_to_time(minutes, start_time="00:00"):
|
| 9 |
start_hour, start_min = map(int, start_time.split(':'))
|
|
|
|
| 12 |
minute = total_minutes % 60
|
| 13 |
return f"{hour:02d}:{minute:02d}"
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def create_simulation_video(frames, specialists_count, second_model_name, fps=24):
|
| 16 |
if not frames:
|
| 17 |
return None
|
|
|
|
| 45 |
axes[0, 1].set_ylim(0, 110)
|
| 46 |
axes[0, 1].set_title(f"ЗАГРУЖЕННОСТЬ СПЕЦИАЛИСТОВ %: {y_load[-1]:.1f}%", fontsize=12, fontweight='bold')
|
| 47 |
|
| 48 |
+
# 3. HEATMAP И ЛЕГЕНДА
|
| 49 |
states = np.array(data['specialist_states'])
|
| 50 |
cols = 20
|
| 51 |
rows = int(np.ceil(specialists_count / cols))
|
|
|
|
| 70 |
q_mod_color = '#991b1b' if data['queue'] > 50 else '#166534'
|
| 71 |
q_biz_color = '#991b1b' if data.get('business_queue', 0) > 50 else '#1e293b'
|
| 72 |
|
|
|
|
| 73 |
ax_stat.text(0.25, 0.9, "ОЧЕРЕДЬ\n(МОДЕЛИ)", fontsize=10, ha='center', fontweight='bold')
|
| 74 |
ax_stat.text(0.25, 0.78, f"{data['queue']}", fontsize=26, ha='center', fontweight='bold', color=q_mod_color)
|
| 75 |
|
|
|
|
| 77 |
ax_stat.text(0.75, 0.78, f"{data.get('business_queue', 0)}", fontsize=26, ha='center', fontweight='bold',
|
| 78 |
color=q_biz_color)
|
| 79 |
|
| 80 |
+
# Сводная таблица
|
| 81 |
cum = data['cumulative']
|
| 82 |
stats_text = (
|
| 83 |
f"Итоговые показатели к {data['time_str']}\n"
|