Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -268,96 +268,106 @@ class ReportGenerator:
|
|
268 |
|
269 |
def create_time_performance_plot(self) -> plt.Figure:
|
270 |
"""Cria o gráfico de relação entre tempo e acertos com visualização otimizada."""
|
271 |
-
|
272 |
-
plt.figure(figsize=(13, 7.5))
|
273 |
-
plt.subplots_adjust(
|
274 |
-
left=0.07, # Margem esquerda reduzida
|
275 |
-
right=0.93, # Margem direita aumentada
|
276 |
-
top=0.92, # Margem superior aumentada
|
277 |
-
bottom=0.12 # Margem inferior reduzida
|
278 |
-
)
|
279 |
-
|
280 |
ax = plt.gca()
|
|
|
|
|
281 |
plt.grid(True, alpha=0.2, linestyle='--')
|
282 |
ax.set_facecolor('#f8f9fa')
|
283 |
-
|
284 |
-
|
285 |
-
|
|
|
|
|
286 |
|
287 |
# Estruturas para armazenamento
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
#
|
292 |
for nivel, color in self.colors.items():
|
293 |
mask = self.data['Nível'] == nivel
|
294 |
tempo = pd.to_timedelta(self.data[mask]['Total Tempo']).dt.total_seconds() / 60
|
295 |
acertos = self.data[mask]['Acertos Absolutos']
|
296 |
|
297 |
-
plt.scatter(tempo, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
298 |
|
299 |
for t, a, nome in zip(tempo, acertos, self.data[mask]['Nome do Aluno']):
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
|
|
|
|
|
|
|
|
312 |
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
|
|
|
|
|
|
|
|
327 |
|
328 |
-
|
329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
330 |
# Calcular centroide do grupo
|
331 |
-
|
332 |
-
|
|
|
333 |
|
334 |
-
|
335 |
-
if len(group) == 1:
|
336 |
-
x, y, nome = group[0]
|
337 |
-
label_text = nome.split()[0]
|
338 |
-
else:
|
339 |
-
x, y = center_x, center_y
|
340 |
-
label_text = "\n".join(sorted(p[2].split()[0] for p in group))
|
341 |
-
|
342 |
-
# Calcular posição otimizada para o label
|
343 |
-
radius = 2.5 + len(group) * 0.5
|
344 |
-
base_angle = np.arctan2(y - np.mean(self.data['Acertos Absolutos']),
|
345 |
-
x - np.mean(pd.to_timedelta(self.data['Total Tempo']).dt.total_seconds() / 60))
|
346 |
|
347 |
-
# Ajustar ângulo baseado na posição
|
348 |
-
if x < np.mean(pd.to_timedelta(self.data['Total Tempo']).dt.total_seconds() / 60):
|
349 |
-
angle = base_angle - np.pi/6
|
350 |
-
else:
|
351 |
-
angle = base_angle + np.pi/6
|
352 |
-
|
353 |
-
label_x = x + radius * np.cos(angle)
|
354 |
-
label_y = y + radius * np.sin(angle)
|
355 |
-
|
356 |
-
# Criar anotação
|
357 |
plt.annotate(
|
358 |
-
|
359 |
-
(
|
360 |
-
xytext=
|
361 |
textcoords='data',
|
362 |
bbox=dict(
|
363 |
facecolor='white',
|
@@ -369,64 +379,60 @@ class ReportGenerator:
|
|
369 |
fontsize=9,
|
370 |
arrowprops=dict(
|
371 |
arrowstyle='-|>',
|
372 |
-
connectionstyle='arc3,rad
|
373 |
color='gray',
|
374 |
-
alpha=0.6
|
375 |
-
mutation_scale=15
|
376 |
)
|
377 |
)
|
|
|
378 |
|
379 |
# Configurações finais
|
380 |
plt.title('Relação entre Tempo e Acertos por Nível', pad=20, fontsize=14)
|
381 |
plt.xlabel('Tempo Total (minutos)', fontsize=12)
|
382 |
plt.ylabel('Número de Acertos', fontsize=12)
|
383 |
-
|
384 |
-
# Legenda
|
385 |
handles, labels = plt.gca().get_legend_handles_labels()
|
386 |
by_label = dict(zip(labels, handles))
|
387 |
-
plt.legend(
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
)
|
396 |
-
|
397 |
return plt.gcf()
|
398 |
|
399 |
def create_tasks_performance_plot(self) -> plt.Figure:
|
400 |
"""Cria o gráfico de relação entre tarefas e acertos com visualização otimizada."""
|
401 |
-
plt.figure(figsize=(
|
402 |
-
plt.subplots_adjust(
|
403 |
-
left=0.07,
|
404 |
-
right=0.93,
|
405 |
-
top=0.92,
|
406 |
-
bottom=0.12
|
407 |
-
)
|
408 |
-
|
409 |
ax = plt.gca()
|
|
|
|
|
410 |
plt.grid(True, alpha=0.2, linestyle='--')
|
411 |
ax.set_facecolor('#f8f9fa')
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
# Coletar e plotar pontos
|
421 |
for nivel, color in self.colors.items():
|
422 |
mask = self.data['Nível'] == nivel
|
423 |
tarefas = self.data[mask]['Tarefas Completadas']
|
424 |
acertos = self.data[mask]['Acertos Absolutos']
|
425 |
-
|
426 |
-
plt.scatter(tarefas, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
427 |
-
|
428 |
for t, a, nome in zip(tarefas, acertos, self.data[mask]['Nome do Aluno']):
|
429 |
-
|
|
|
|
|
|
|
|
|
430 |
|
431 |
# Linha de tendência
|
432 |
z = np.polyfit(self.data['Tarefas Completadas'],
|
@@ -445,41 +451,45 @@ class ReportGenerator:
|
|
445 |
plt.axhline(y=media_acertos, color='gray', linestyle=':', alpha=0.5,
|
446 |
label='Média de Acertos')
|
447 |
|
448 |
-
#
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
label_y = y + radius * np.sin(angle)
|
477 |
|
478 |
-
|
|
|
|
|
|
|
|
|
|
|
479 |
plt.annotate(
|
480 |
-
|
481 |
(x, y),
|
482 |
-
xytext=
|
483 |
textcoords='data',
|
484 |
bbox=dict(
|
485 |
facecolor='white',
|
@@ -491,21 +501,46 @@ class ReportGenerator:
|
|
491 |
fontsize=9,
|
492 |
arrowprops=dict(
|
493 |
arrowstyle='-|>',
|
494 |
-
connectionstyle='arc3,rad
|
495 |
color='gray',
|
496 |
-
alpha=0.6
|
497 |
-
mutation_scale=15
|
498 |
)
|
499 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
|
501 |
# Configurações finais
|
502 |
plt.title('Relação entre Tarefas Completadas e Acertos', pad=20, fontsize=14)
|
503 |
plt.xlabel('Número de Tarefas Completadas', fontsize=12)
|
504 |
plt.ylabel('Número de Acertos', fontsize=12)
|
505 |
-
|
506 |
-
# Forçar ticks inteiros no eixo x
|
507 |
-
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
508 |
-
|
509 |
# Ajustar limites
|
510 |
plt.xlim(
|
511 |
self.data['Tarefas Completadas'].min() - 1,
|
@@ -515,22 +550,20 @@ class ReportGenerator:
|
|
515 |
max(0, self.data['Acertos Absolutos'].min() - 1),
|
516 |
self.data['Acertos Absolutos'].max() + 2
|
517 |
)
|
518 |
-
|
519 |
-
# Legenda
|
520 |
handles, labels = plt.gca().get_legend_handles_labels()
|
521 |
by_label = dict(zip(labels, handles))
|
522 |
-
plt.legend(
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
frameon=True,
|
529 |
-
fancybox=True
|
530 |
-
)
|
531 |
-
|
532 |
-
return plt.gcf()
|
533 |
|
|
|
|
|
|
|
534 |
def generate_graphs(self) -> List[plt.Figure]:
|
535 |
"""Gera todos os gráficos para o relatório."""
|
536 |
try:
|
|
|
268 |
|
269 |
def create_time_performance_plot(self) -> plt.Figure:
|
270 |
"""Cria o gráfico de relação entre tempo e acertos com visualização otimizada."""
|
271 |
+
plt.figure(figsize=(15, 10))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
272 |
ax = plt.gca()
|
273 |
+
|
274 |
+
# Configuração inicial com estilo limpo
|
275 |
plt.grid(True, alpha=0.2, linestyle='--')
|
276 |
ax.set_facecolor('#f8f9fa')
|
277 |
+
|
278 |
+
# Linha de média
|
279 |
+
media_acertos = self.data['Acertos Absolutos'].mean()
|
280 |
+
plt.axhline(y=media_acertos, color='gray', linestyle=':', alpha=0.5,
|
281 |
+
label='Média de Acertos')
|
282 |
|
283 |
# Estruturas para armazenamento
|
284 |
+
all_points = []
|
285 |
+
point_groups = {}
|
286 |
+
|
287 |
+
# Plotar pontos e coletar dados
|
288 |
for nivel, color in self.colors.items():
|
289 |
mask = self.data['Nível'] == nivel
|
290 |
tempo = pd.to_timedelta(self.data[mask]['Total Tempo']).dt.total_seconds() / 60
|
291 |
acertos = self.data[mask]['Acertos Absolutos']
|
292 |
|
293 |
+
scatter = plt.scatter(tempo, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
294 |
|
295 |
for t, a, nome in zip(tempo, acertos, self.data[mask]['Nome do Aluno']):
|
296 |
+
point_key = (round(t, 1), a)
|
297 |
+
if point_key not in point_groups:
|
298 |
+
point_groups[point_key] = []
|
299 |
+
point_groups[point_key].append((t, a, nome, color))
|
300 |
+
all_points.append((t, a))
|
301 |
+
|
302 |
+
# Lista para controle de sobreposições
|
303 |
+
annotations = []
|
304 |
+
|
305 |
+
# Função para calcular melhor posição
|
306 |
+
def get_best_position(x, y, existing_annotations):
|
307 |
+
angles = np.linspace(0, 2*np.pi, 16) # 16 direções possíveis
|
308 |
+
base_radius = 3.0
|
309 |
+
max_radius = 6.0
|
310 |
+
best_pos = None
|
311 |
+
min_overlap = float('inf')
|
312 |
|
313 |
+
for radius in np.linspace(base_radius, max_radius, 4):
|
314 |
+
for angle in angles:
|
315 |
+
new_x = x + radius * np.cos(angle)
|
316 |
+
new_y = y + radius * np.sin(angle)
|
317 |
+
|
318 |
+
# Verificar limites do gráfico
|
319 |
+
if not (ax.get_xlim()[0] <= new_x <= ax.get_xlim()[1] and
|
320 |
+
ax.get_ylim()[0] <= new_y <= ax.get_ylim()[1]):
|
321 |
+
continue
|
322 |
+
|
323 |
+
overlaps = sum(1 for ann in existing_annotations if
|
324 |
+
abs(ann[0] - new_x) < 2 and abs(ann[1] - new_y) < 2)
|
325 |
+
|
326 |
+
if overlaps < min_overlap:
|
327 |
+
min_overlap = overlaps
|
328 |
+
best_pos = (new_x, new_y)
|
329 |
+
|
330 |
+
return best_pos or (x + base_radius, y + base_radius)
|
331 |
|
332 |
+
# Adicionar anotações
|
333 |
+
for key, group in point_groups.items():
|
334 |
+
if len(group) == 1:
|
335 |
+
x, y, nome, _ = group[0]
|
336 |
+
new_pos = get_best_position(x, y, annotations)
|
337 |
+
|
338 |
+
plt.annotate(
|
339 |
+
nome.split()[0],
|
340 |
+
(x, y),
|
341 |
+
xytext=new_pos,
|
342 |
+
textcoords='data',
|
343 |
+
bbox=dict(
|
344 |
+
facecolor='white',
|
345 |
+
edgecolor='lightgray',
|
346 |
+
alpha=0.95,
|
347 |
+
pad=1.0,
|
348 |
+
boxstyle='round,pad=0.8'
|
349 |
+
),
|
350 |
+
fontsize=9,
|
351 |
+
arrowprops=dict(
|
352 |
+
arrowstyle='-|>',
|
353 |
+
connectionstyle='arc3,rad=0.2',
|
354 |
+
color='gray',
|
355 |
+
alpha=0.6
|
356 |
+
)
|
357 |
+
)
|
358 |
+
annotations.append(new_pos)
|
359 |
+
else:
|
360 |
# Calcular centroide do grupo
|
361 |
+
x_center = sum(p[0] for p in group) / len(group)
|
362 |
+
y_center = sum(p[1] for p in group) / len(group)
|
363 |
+
nomes = sorted(set([p[2].split()[0] for p in group]))
|
364 |
|
365 |
+
new_pos = get_best_position(x_center, y_center, annotations)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
367 |
plt.annotate(
|
368 |
+
f"{len(group)} alunos:\n" + "\n".join(nomes),
|
369 |
+
(x_center, y_center),
|
370 |
+
xytext=new_pos,
|
371 |
textcoords='data',
|
372 |
bbox=dict(
|
373 |
facecolor='white',
|
|
|
379 |
fontsize=9,
|
380 |
arrowprops=dict(
|
381 |
arrowstyle='-|>',
|
382 |
+
connectionstyle='arc3,rad=0.2',
|
383 |
color='gray',
|
384 |
+
alpha=0.6
|
|
|
385 |
)
|
386 |
)
|
387 |
+
annotations.append(new_pos)
|
388 |
|
389 |
# Configurações finais
|
390 |
plt.title('Relação entre Tempo e Acertos por Nível', pad=20, fontsize=14)
|
391 |
plt.xlabel('Tempo Total (minutos)', fontsize=12)
|
392 |
plt.ylabel('Número de Acertos', fontsize=12)
|
393 |
+
|
394 |
+
# Legenda com todos os elementos
|
395 |
handles, labels = plt.gca().get_legend_handles_labels()
|
396 |
by_label = dict(zip(labels, handles))
|
397 |
+
plt.legend(by_label.values(), by_label.keys(),
|
398 |
+
bbox_to_anchor=(1.05, 1),
|
399 |
+
loc='upper left',
|
400 |
+
borderaxespad=0,
|
401 |
+
frameon=True,
|
402 |
+
fancybox=True)
|
403 |
+
|
404 |
+
plt.tight_layout()
|
|
|
|
|
405 |
return plt.gcf()
|
406 |
|
407 |
def create_tasks_performance_plot(self) -> plt.Figure:
|
408 |
"""Cria o gráfico de relação entre tarefas e acertos com visualização otimizada."""
|
409 |
+
plt.figure(figsize=(15, 10))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
410 |
ax = plt.gca()
|
411 |
+
|
412 |
+
# Configuração inicial
|
413 |
plt.grid(True, alpha=0.2, linestyle='--')
|
414 |
ax.set_facecolor('#f8f9fa')
|
415 |
+
|
416 |
+
# Forçar ticks inteiros no eixo x
|
417 |
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
418 |
+
|
419 |
+
point_groups = {}
|
420 |
+
all_points = []
|
421 |
+
|
422 |
+
# Plotar pontos e coletar dados
|
|
|
423 |
for nivel, color in self.colors.items():
|
424 |
mask = self.data['Nível'] == nivel
|
425 |
tarefas = self.data[mask]['Tarefas Completadas']
|
426 |
acertos = self.data[mask]['Acertos Absolutos']
|
427 |
+
|
428 |
+
scatter = plt.scatter(tarefas, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
429 |
+
|
430 |
for t, a, nome in zip(tarefas, acertos, self.data[mask]['Nome do Aluno']):
|
431 |
+
point_key = (t, a)
|
432 |
+
if point_key not in point_groups:
|
433 |
+
point_groups[point_key] = []
|
434 |
+
point_groups[point_key].append((t, a, nome, color))
|
435 |
+
all_points.append((t, a))
|
436 |
|
437 |
# Linha de tendência
|
438 |
z = np.polyfit(self.data['Tarefas Completadas'],
|
|
|
451 |
plt.axhline(y=media_acertos, color='gray', linestyle=':', alpha=0.5,
|
452 |
label='Média de Acertos')
|
453 |
|
454 |
+
# Lista para controle de sobreposições
|
455 |
+
annotations = []
|
456 |
+
|
457 |
+
# Função para calcular melhor posição
|
458 |
+
def get_best_position(x, y, existing_annotations):
|
459 |
+
angles = np.linspace(0, 2*np.pi, 16)
|
460 |
+
base_radius = 2.0 # Menor para tarefas discretas
|
461 |
+
max_radius = 4.0
|
462 |
+
best_pos = None
|
463 |
+
min_overlap = float('inf')
|
464 |
+
|
465 |
+
for radius in np.linspace(base_radius, max_radius, 4):
|
466 |
+
for angle in angles:
|
467 |
+
new_x = x + radius * np.cos(angle)
|
468 |
+
new_y = y + radius * np.sin(angle)
|
469 |
+
|
470 |
+
if not (ax.get_xlim()[0] <= new_x <= ax.get_xlim()[1] and
|
471 |
+
ax.get_ylim()[0] <= new_y <= ax.get_ylim()[1]):
|
472 |
+
continue
|
473 |
+
|
474 |
+
overlaps = sum(1 for ann in existing_annotations if
|
475 |
+
abs(ann[0] - new_x) < 1.5 and abs(ann[1] - new_y) < 1.5)
|
476 |
+
|
477 |
+
if overlaps < min_overlap:
|
478 |
+
min_overlap = overlaps
|
479 |
+
best_pos = (new_x, new_y)
|
480 |
+
|
481 |
+
return best_pos or (x + base_radius, y + base_radius)
|
|
|
482 |
|
483 |
+
# Adicionar anotações
|
484 |
+
for key, group in point_groups.items():
|
485 |
+
if len(group) == 1:
|
486 |
+
x, y, nome, _ = group[0]
|
487 |
+
new_pos = get_best_position(x, y, annotations)
|
488 |
+
|
489 |
plt.annotate(
|
490 |
+
nome.split()[0],
|
491 |
(x, y),
|
492 |
+
xytext=new_pos,
|
493 |
textcoords='data',
|
494 |
bbox=dict(
|
495 |
facecolor='white',
|
|
|
501 |
fontsize=9,
|
502 |
arrowprops=dict(
|
503 |
arrowstyle='-|>',
|
504 |
+
connectionstyle='arc3,rad=0.2',
|
505 |
color='gray',
|
506 |
+
alpha=0.6
|
|
|
507 |
)
|
508 |
)
|
509 |
+
annotations.append(new_pos)
|
510 |
+
else:
|
511 |
+
x_center = sum(p[0] for p in group) / len(group)
|
512 |
+
y_center = sum(p[1] for p in group) / len(group)
|
513 |
+
nomes = sorted(set([p[2].split()[0] for p in group]))
|
514 |
+
|
515 |
+
new_pos = get_best_position(x_center, y_center, annotations)
|
516 |
+
|
517 |
+
plt.annotate(
|
518 |
+
f"{len(group)} alunos com\n{group[0][0]:.0f} tarefas:\n" + "\n".join(nomes),
|
519 |
+
(x_center, y_center),
|
520 |
+
xytext=new_pos,
|
521 |
+
textcoords='data',
|
522 |
+
bbox=dict(
|
523 |
+
facecolor='white',
|
524 |
+
edgecolor='lightgray',
|
525 |
+
alpha=0.95,
|
526 |
+
pad=1.0,
|
527 |
+
boxstyle='round,pad=0.8'
|
528 |
+
),
|
529 |
+
fontsize=9,
|
530 |
+
arrowprops=dict(
|
531 |
+
arrowstyle='-|>',
|
532 |
+
connectionstyle='arc3,rad=0.2',
|
533 |
+
color='gray',
|
534 |
+
alpha=0.6
|
535 |
+
)
|
536 |
+
)
|
537 |
+
annotations.append(new_pos)
|
538 |
|
539 |
# Configurações finais
|
540 |
plt.title('Relação entre Tarefas Completadas e Acertos', pad=20, fontsize=14)
|
541 |
plt.xlabel('Número de Tarefas Completadas', fontsize=12)
|
542 |
plt.ylabel('Número de Acertos', fontsize=12)
|
543 |
+
|
|
|
|
|
|
|
544 |
# Ajustar limites
|
545 |
plt.xlim(
|
546 |
self.data['Tarefas Completadas'].min() - 1,
|
|
|
550 |
max(0, self.data['Acertos Absolutos'].min() - 1),
|
551 |
self.data['Acertos Absolutos'].max() + 2
|
552 |
)
|
553 |
+
|
554 |
+
# Legenda com todos os elementos
|
555 |
handles, labels = plt.gca().get_legend_handles_labels()
|
556 |
by_label = dict(zip(labels, handles))
|
557 |
+
plt.legend(by_label.values(), by_label.keys(),
|
558 |
+
bbox_to_anchor=(1.05, 1),
|
559 |
+
loc='upper left',
|
560 |
+
borderaxespad=0,
|
561 |
+
frameon=True,
|
562 |
+
fancybox=True)
|
|
|
|
|
|
|
|
|
|
|
563 |
|
564 |
+
plt.tight_layout()
|
565 |
+
return plt.gcf()
|
566 |
+
|
567 |
def generate_graphs(self) -> List[plt.Figure]:
|
568 |
"""Gera todos os gráficos para o relatório."""
|
569 |
try:
|