Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -270,52 +270,47 @@ class ReportGenerator:
|
|
270 |
"""Cria o gráfico de relação entre tempo e acertos com agrupamento melhorado."""
|
271 |
plt.figure(figsize=(15, 10))
|
272 |
|
273 |
-
# Configuração inicial
|
274 |
plt.grid(True, alpha=0.2, linestyle='--')
|
275 |
plt.gca().set_facecolor('#f8f9fa')
|
276 |
|
277 |
-
# Função para calcular
|
278 |
-
def
|
279 |
-
|
|
|
|
|
280 |
|
281 |
-
#
|
282 |
-
|
|
|
|
|
|
|
|
|
|
|
283 |
|
284 |
-
#
|
285 |
for nivel, color in self.colors.items():
|
286 |
mask = self.data['Nível'] == nivel
|
287 |
tempo = pd.to_timedelta(self.data[mask]['Total Tempo']).dt.total_seconds() / 60
|
288 |
acertos = self.data[mask]['Acertos Absolutos']
|
289 |
|
290 |
-
plt.scatter(tempo, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
291 |
|
|
|
292 |
for t, a, nome in zip(tempo, acertos, self.data[mask]['Nome do Aluno']):
|
293 |
-
|
|
|
|
|
|
|
294 |
|
295 |
-
#
|
296 |
-
|
297 |
-
grouped_points = []
|
298 |
-
processed = set()
|
299 |
-
|
300 |
-
for i, (x1, y1, name1, color1) in enumerate(all_points):
|
301 |
-
if i in processed:
|
302 |
-
continue
|
303 |
-
|
304 |
-
current_group = [(x1, y1, name1, color1)]
|
305 |
-
processed.add(i)
|
306 |
-
|
307 |
-
for j, (x2, y2, name2, color2) in enumerate(all_points[i+1:], i+1):
|
308 |
-
if j not in processed and calculate_distance((x1, y1), (x2, y2)) < distance_threshold:
|
309 |
-
current_group.append((x2, y2, name2, color2))
|
310 |
-
processed.add(j)
|
311 |
-
|
312 |
-
grouped_points.append(current_group)
|
313 |
|
314 |
-
# Adicionar anotações
|
315 |
-
for
|
316 |
-
if len(
|
317 |
-
x, y, nome, _ =
|
318 |
-
plt.annotate(
|
319 |
nome.split()[0],
|
320 |
(x, y),
|
321 |
xytext=(5, 5),
|
@@ -328,21 +323,19 @@ class ReportGenerator:
|
|
328 |
),
|
329 |
fontsize=8
|
330 |
)
|
|
|
331 |
else:
|
332 |
-
# Calcular
|
333 |
-
|
334 |
-
|
335 |
|
336 |
-
#
|
337 |
-
|
|
|
338 |
|
339 |
-
|
340 |
-
x_direction = 30 if x_center < np.mean([p[0] for p in all_points]) else -30
|
341 |
-
y_direction = 20 if y_center < np.mean([p[1] for p in all_points]) else -20
|
342 |
-
|
343 |
-
plt.annotate(
|
344 |
'\n'.join(nomes),
|
345 |
-
(
|
346 |
xytext=(x_direction, y_direction),
|
347 |
textcoords='offset points',
|
348 |
bbox=dict(
|
@@ -360,11 +353,25 @@ class ReportGenerator:
|
|
360 |
alpha=0.6
|
361 |
)
|
362 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
363 |
|
364 |
-
plt.title('Relação entre Tempo e Acertos por Nível', pad=20)
|
365 |
-
plt.xlabel('Tempo Total (minutos)')
|
366 |
-
plt.ylabel('Número de Acertos')
|
367 |
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
|
|
|
|
|
|
|
368 |
plt.tight_layout()
|
369 |
|
370 |
return plt.gcf()
|
@@ -377,75 +384,141 @@ class ReportGenerator:
|
|
377 |
plt.grid(True, alpha=0.2, linestyle='--')
|
378 |
plt.gca().set_facecolor('#f8f9fa')
|
379 |
|
380 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
381 |
point_groups = {}
|
|
|
382 |
|
383 |
-
#
|
384 |
for nivel, color in self.colors.items():
|
385 |
mask = self.data['Nível'] == nivel
|
386 |
tarefas = self.data[mask]['Tarefas Completadas']
|
387 |
acertos = self.data[mask]['Acertos Absolutos']
|
388 |
|
389 |
-
plt.scatter(tarefas, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
390 |
-
|
391 |
-
# Agrupar pontos com coordenadas idênticas
|
392 |
for t, a, nome in zip(tarefas, acertos, self.data[mask]['Nome do Aluno']):
|
393 |
-
|
394 |
-
if
|
395 |
-
point_groups[
|
396 |
-
point_groups[
|
|
|
397 |
|
398 |
-
#
|
399 |
z = np.polyfit(self.data['Tarefas Completadas'],
|
400 |
-
|
401 |
p = np.poly1d(z)
|
402 |
x_range = np.linspace(
|
403 |
-
self.data['Tarefas Completadas'].min(),
|
404 |
-
self.data['Tarefas Completadas'].max(),
|
405 |
100
|
406 |
)
|
407 |
plt.plot(x_range, p(x_range), "--", color='#e74c3c', alpha=0.8,
|
408 |
label='Tendência', linewidth=2)
|
409 |
|
410 |
-
#
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
420 |
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
color='gray',
|
441 |
-
alpha=0.6
|
442 |
)
|
443 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
444 |
|
445 |
-
plt.title('Relação entre Tarefas Completadas e Acertos', pad=20)
|
446 |
-
plt.xlabel('Número de Tarefas Completadas')
|
447 |
-
plt.ylabel('Número de Acertos')
|
448 |
-
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
|
449 |
plt.tight_layout()
|
450 |
|
451 |
return plt.gcf()
|
|
|
270 |
"""Cria o gráfico de relação entre tempo e acertos com agrupamento melhorado."""
|
271 |
plt.figure(figsize=(15, 10))
|
272 |
|
273 |
+
# Configuração inicial com estilo mais limpo
|
274 |
plt.grid(True, alpha=0.2, linestyle='--')
|
275 |
plt.gca().set_facecolor('#f8f9fa')
|
276 |
|
277 |
+
# Função para calcular centroide de um grupo de pontos
|
278 |
+
def calculate_centroid(points):
|
279 |
+
x = sum(p[0] for p in points) / len(points)
|
280 |
+
y = sum(p[1] for p in points) / len(points)
|
281 |
+
return (x, y)
|
282 |
|
283 |
+
# Função para verificar sobreposição de textos
|
284 |
+
def texts_overlap(bbox1, bbox2, min_dist=40):
|
285 |
+
return (abs(bbox1.x0 - bbox2.x0) < min_dist and
|
286 |
+
abs(bbox1.y0 - bbox2.y0) < min_dist)
|
287 |
+
|
288 |
+
# Estrutura para armazenar pontos e suas informações
|
289 |
+
point_groups = {}
|
290 |
|
291 |
+
# Plotar pontos e coletar informações
|
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 |
+
scatter = plt.scatter(tempo, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
298 |
|
299 |
+
# Agrupar pontos próximos
|
300 |
for t, a, nome in zip(tempo, acertos, self.data[mask]['Nome do Aluno']):
|
301 |
+
point_key = (round(t, 1), a) # Arredondar tempo para agrupar pontos próximos
|
302 |
+
if point_key not in point_groups:
|
303 |
+
point_groups[point_key] = []
|
304 |
+
point_groups[point_key].append((t, a, nome, color))
|
305 |
|
306 |
+
# Lista para armazenar todos os textos para ajuste posterior
|
307 |
+
texts = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
308 |
|
309 |
+
# Adicionar anotações com posicionamento otimizado
|
310 |
+
for points in point_groups.values():
|
311 |
+
if len(points) == 1:
|
312 |
+
x, y, nome, _ = points[0]
|
313 |
+
text = plt.annotate(
|
314 |
nome.split()[0],
|
315 |
(x, y),
|
316 |
xytext=(5, 5),
|
|
|
323 |
),
|
324 |
fontsize=8
|
325 |
)
|
326 |
+
texts.append(text)
|
327 |
else:
|
328 |
+
# Calcular posição central do grupo
|
329 |
+
cx, cy = calculate_centroid([(p[0], p[1]) for p in points])
|
330 |
+
nomes = sorted([p[2].split()[0] for p in points])
|
331 |
|
332 |
+
# Determinar direção baseada na posição no gráfico
|
333 |
+
x_direction = 30 if cx < np.mean([p[0] for p in points]) else -30
|
334 |
+
y_direction = 20 if cy < np.mean([p[1] for p in points]) else -20
|
335 |
|
336 |
+
text = plt.annotate(
|
|
|
|
|
|
|
|
|
337 |
'\n'.join(nomes),
|
338 |
+
(cx, cy),
|
339 |
xytext=(x_direction, y_direction),
|
340 |
textcoords='offset points',
|
341 |
bbox=dict(
|
|
|
353 |
alpha=0.6
|
354 |
)
|
355 |
)
|
356 |
+
texts.append(text)
|
357 |
+
|
358 |
+
# Ajustar posições dos textos para evitar sobreposições
|
359 |
+
adjust_text(
|
360 |
+
texts,
|
361 |
+
expand_points=(1.2, 1.2),
|
362 |
+
expand_text=(1.1, 1.1),
|
363 |
+
force_points=(0.5, 0.5),
|
364 |
+
force_text=(0.5, 0.5),
|
365 |
+
arrowprops=dict(arrowstyle='->', color='gray', alpha=0.6)
|
366 |
+
)
|
367 |
|
368 |
+
plt.title('Relação entre Tempo e Acertos por Nível', pad=20, fontsize=14)
|
369 |
+
plt.xlabel('Tempo Total (minutos)', fontsize=12)
|
370 |
+
plt.ylabel('Número de Acertos', fontsize=12)
|
371 |
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
|
372 |
+
|
373 |
+
# Ajustar limites e margens
|
374 |
+
plt.margins(x=0.1, y=0.1)
|
375 |
plt.tight_layout()
|
376 |
|
377 |
return plt.gcf()
|
|
|
384 |
plt.grid(True, alpha=0.2, linestyle='--')
|
385 |
plt.gca().set_facecolor('#f8f9fa')
|
386 |
|
387 |
+
# Função para calcular centroide de um grupo de pontos
|
388 |
+
def calculate_centroid(points):
|
389 |
+
x = sum(p[0] for p in points) / len(points)
|
390 |
+
y = sum(p[1] for p in points) / len(points)
|
391 |
+
return (x, y)
|
392 |
+
|
393 |
+
# Estrutura para armazenar pontos e suas informações
|
394 |
point_groups = {}
|
395 |
+
all_points = []
|
396 |
|
397 |
+
# Plotar pontos e coletar informações
|
398 |
for nivel, color in self.colors.items():
|
399 |
mask = self.data['Nível'] == nivel
|
400 |
tarefas = self.data[mask]['Tarefas Completadas']
|
401 |
acertos = self.data[mask]['Acertos Absolutos']
|
402 |
|
403 |
+
scatter = plt.scatter(tarefas, acertos, c=color, label=nivel, alpha=0.7, s=100)
|
404 |
+
|
405 |
+
# Agrupar pontos com coordenadas idênticas ou muito próximas
|
406 |
for t, a, nome in zip(tarefas, acertos, self.data[mask]['Nome do Aluno']):
|
407 |
+
point_key = (t, a) # Pontos exatamente iguais
|
408 |
+
if point_key not in point_groups:
|
409 |
+
point_groups[point_key] = []
|
410 |
+
point_groups[point_key].append((t, a, nome, color))
|
411 |
+
all_points.append((t, a))
|
412 |
|
413 |
+
# Calcular e plotar linha de tendência
|
414 |
z = np.polyfit(self.data['Tarefas Completadas'],
|
415 |
+
self.data['Acertos Absolutos'], 1)
|
416 |
p = np.poly1d(z)
|
417 |
x_range = np.linspace(
|
418 |
+
self.data['Tarefas Completadas'].min() - 0.5,
|
419 |
+
self.data['Tarefas Completadas'].max() + 0.5,
|
420 |
100
|
421 |
)
|
422 |
plt.plot(x_range, p(x_range), "--", color='#e74c3c', alpha=0.8,
|
423 |
label='Tendência', linewidth=2)
|
424 |
|
425 |
+
# Lista para armazenar todos os textos para ajuste posterior
|
426 |
+
texts = []
|
427 |
+
|
428 |
+
# Calcular média de acertos para referência
|
429 |
+
media_acertos = self.data['Acertos Absolutos'].mean()
|
430 |
+
plt.axhline(y=media_acertos, color='gray', linestyle=':', alpha=0.5,
|
431 |
+
label='Média de Acertos')
|
432 |
+
|
433 |
+
# Adicionar anotações com posicionamento otimizado
|
434 |
+
for points in point_groups.values():
|
435 |
+
if len(points) == 1:
|
436 |
+
x, y, nome, _ = points[0]
|
437 |
+
text = plt.annotate(
|
438 |
+
nome.split()[0],
|
439 |
+
(x, y),
|
440 |
+
xytext=(5, 5),
|
441 |
+
textcoords='offset points',
|
442 |
+
bbox=dict(
|
443 |
+
facecolor='white',
|
444 |
+
edgecolor='none',
|
445 |
+
alpha=0.8,
|
446 |
+
pad=0.5
|
447 |
+
),
|
448 |
+
fontsize=8
|
449 |
+
)
|
450 |
+
texts.append(text)
|
451 |
+
else:
|
452 |
+
# Calcular posição central do grupo
|
453 |
+
cx, cy = calculate_centroid([(p[0], p[1]) for p in points])
|
454 |
+
nomes = sorted([p[2].split()[0] for p in points])
|
455 |
+
|
456 |
+
# Calcular direção baseada na densidade de pontos na vizinhança
|
457 |
+
nearby_points = sum(1 for px, py in all_points
|
458 |
+
if abs(px - cx) <= 1 and abs(py - cy) <= 1)
|
459 |
+
angle = (hash(str(nomes)) % 8) * 45 # Ângulo variável baseado nos nomes
|
460 |
+
radius = 20 + (nearby_points * 5) # Raio aumenta com a densidade
|
461 |
+
|
462 |
+
x_direction = radius * np.cos(np.radians(angle))
|
463 |
+
y_direction = radius * np.sin(np.radians(angle))
|
464 |
|
465 |
+
text = plt.annotate(
|
466 |
+
'\n'.join(nomes),
|
467 |
+
(cx, cy),
|
468 |
+
xytext=(x_direction, y_direction),
|
469 |
+
textcoords='offset points',
|
470 |
+
bbox=dict(
|
471 |
+
facecolor='white',
|
472 |
+
edgecolor='lightgray',
|
473 |
+
alpha=0.9,
|
474 |
+
pad=0.5,
|
475 |
+
boxstyle='round,pad=0.5'
|
476 |
+
),
|
477 |
+
fontsize=8,
|
478 |
+
arrowprops=dict(
|
479 |
+
arrowstyle='-|>',
|
480 |
+
connectionstyle=f'arc3,rad={0.2 if x_direction > 0 else -0.2}',
|
481 |
+
color='gray',
|
482 |
+
alpha=0.6
|
483 |
+
)
|
|
|
|
|
484 |
)
|
485 |
+
texts.append(text)
|
486 |
+
|
487 |
+
# Ajustar posições dos textos para evitar sobreposições
|
488 |
+
adjust_text(
|
489 |
+
texts,
|
490 |
+
expand_points=(1.2, 1.2),
|
491 |
+
expand_text=(1.1, 1.1),
|
492 |
+
force_points=(0.5, 0.5),
|
493 |
+
force_text=(0.5, 0.5),
|
494 |
+
arrowprops=dict(arrowstyle='->', color='gray', alpha=0.6)
|
495 |
+
)
|
496 |
+
|
497 |
+
# Configurações finais do gráfico
|
498 |
+
plt.title('Relação entre Tarefas Completadas e Acertos', pad=20, fontsize=14)
|
499 |
+
plt.xlabel('Número de Tarefas Completadas', fontsize=12)
|
500 |
+
plt.ylabel('Número de Acertos', fontsize=12)
|
501 |
+
|
502 |
+
# Ajustar limites dos eixos para melhor visualização
|
503 |
+
plt.xlim(
|
504 |
+
self.data['Tarefas Completadas'].min() - 1,
|
505 |
+
self.data['Tarefas Completadas'].max() + 1
|
506 |
+
)
|
507 |
+
plt.ylim(
|
508 |
+
self.data['Acertos Absolutos'].min() - 1,
|
509 |
+
self.data['Acertos Absolutos'].max() + 2
|
510 |
+
)
|
511 |
+
|
512 |
+
# Configurar legenda com todos os elementos
|
513 |
+
plt.legend(
|
514 |
+
bbox_to_anchor=(1.05, 1),
|
515 |
+
loc='upper left',
|
516 |
+
borderaxespad=0,
|
517 |
+
frameon=True,
|
518 |
+
fancybox=True,
|
519 |
+
shadow=True
|
520 |
+
)
|
521 |
|
|
|
|
|
|
|
|
|
522 |
plt.tight_layout()
|
523 |
|
524 |
return plt.gcf()
|