histlearn commited on
Commit
b0f7407
·
verified ·
1 Parent(s): eceac56

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +14 -495
app.py CHANGED
@@ -38,38 +38,9 @@ CONCEITOS_VALIDOS = ['ES', 'EP', 'ET']
38
  COR_APROVADO = '#2ECC71' # Verde suave
39
  COR_REPROVADO = '#E74C3C' # Vermelho suave
40
 
41
- # Definição das disciplinas de formação básica
42
- FORMACAO_BASICA = {
43
- 'fundamental': {
44
- 'LINGUA PORTUGUESA',
45
- 'MATEMATICA',
46
- 'HISTORIA',
47
- 'GEOGRAFIA',
48
- 'CIENCIAS',
49
- 'LINGUA ESTRANGEIRA INGLES',
50
- 'ARTE',
51
- 'EDUCACAO FISICA'
52
- },
53
- 'medio': {
54
- 'LINGUA PORTUGUESA',
55
- 'MATEMATICA',
56
- 'HISTORIA',
57
- 'GEOGRAFIA',
58
- 'BIOLOGIA',
59
- 'FISICA',
60
- 'QUIMICA',
61
- 'INGLES',
62
- 'FILOSOFIA',
63
- 'SOCIOLOGIA',
64
- 'ARTE',
65
- 'EDUCACAO FISICA'
66
- }
67
- }
68
-
69
  # Context managers
70
  @contextmanager
71
  def temp_directory():
72
- """Context manager para diretório temporário."""
73
  temp_dir = tempfile.mkdtemp()
74
  try:
75
  yield temp_dir
@@ -79,7 +50,6 @@ def temp_directory():
79
 
80
  @contextmanager
81
  def temp_file(suffix=None):
82
- """Context manager para arquivo temporário."""
83
  temp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
84
  try:
85
  yield temp.name
@@ -88,13 +58,11 @@ def temp_file(suffix=None):
88
  os.unlink(temp.name)
89
 
90
  class PDFReport(FPDF):
91
- """Classe personalizada para geração do relatório PDF."""
92
  def __init__(self):
93
  super().__init__()
94
  self.set_auto_page_break(auto=True, margin=15)
95
 
96
  def header_footer(self):
97
- """Adiciona header e footer padrão nas páginas."""
98
  self.set_y(-30)
99
  self.line(10, self.get_y(), 200, self.get_y())
100
  self.ln(5)
@@ -103,491 +71,42 @@ class PDFReport(FPDF):
103
  'Este relatório é uma análise automática e deve ser validado junto à secretaria da escola.',
104
  0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
105
 
106
- def converter_nota(valor) -> Optional[float]:
107
- """Converte valor de nota para float, tratando casos especiais e conceitos."""
108
- if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None':
109
- return None
110
-
111
- if isinstance(valor, str):
112
- valor_limpo = valor.strip().upper()
113
- if valor_limpo in CONCEITOS_VALIDOS:
114
- conceitos_map = {'ET': 10, 'ES': 8, 'EP': 6}
115
- return conceitos_map.get(valor_limpo)
116
-
117
- try:
118
- return float(valor_limpo.replace(',', '.'))
119
- except:
120
- return None
121
-
122
- if isinstance(valor, (int, float)):
123
- return float(valor)
124
-
125
- return None
126
-
127
- def calcular_media_bimestres(notas: List[float]) -> float:
128
- """Calcula média considerando apenas bimestres com notas válidas."""
129
- notas_validas = [nota for nota in notas if nota is not None]
130
- return sum(notas_validas) / len(notas_validas) if notas_validas else 0
131
-
132
- def calcular_frequencia_media(frequencias: List[str]) -> float:
133
- """Calcula média de frequência considerando apenas bimestres cursados."""
134
- freq_validas = []
135
- for freq in frequencias:
136
- try:
137
- if isinstance(freq, str):
138
- freq = freq.strip().replace('%', '').replace(',', '.')
139
- if freq and freq != '-':
140
- valor = float(freq)
141
- if valor > 0:
142
- freq_validas.append(valor)
143
- except:
144
- continue
145
-
146
- return sum(freq_validas) / len(freq_validas) if freq_validas else 0
147
-
148
- def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
149
- """Extrai tabelas do PDF usando stream para o nome e lattice para notas."""
150
- try:
151
- # Extrair nome do aluno usando stream
152
- tables_header = camelot.read_pdf(
153
- pdf_path,
154
- pages='1',
155
- flavor='stream',
156
- edge_tol=500
157
- )
158
-
159
- info_aluno = {}
160
-
161
- # Procurar nome do aluno
162
- for table in tables_header:
163
- df = table.df
164
- for i in range(len(df)):
165
- for j in range(len(df.columns)):
166
- texto = str(df.iloc[i,j]).strip()
167
- if 'Nome do Aluno' in texto:
168
- try:
169
- if j + 1 < len(df.columns):
170
- nome = str(df.iloc[i,j+1]).strip()
171
- elif i + 1 < len(df):
172
- nome = str(df.iloc[i+1,j]).strip()
173
- if nome and nome != 'Nome do Aluno:':
174
- info_aluno['nome'] = nome
175
- break
176
- except:
177
- continue
178
-
179
- # Extrair tabela de notas usando lattice
180
- tables_notas = camelot.read_pdf(
181
- pdf_path,
182
- pages='all',
183
- flavor='lattice'
184
- )
185
-
186
- # Encontrar tabela de notas
187
- df_notas = None
188
- max_rows = 0
189
-
190
- for table in tables_notas:
191
- df_temp = table.df
192
- if len(df_temp) > max_rows and 'Disciplina' in str(df_temp.iloc[0,0]):
193
- max_rows = len(df_temp)
194
- df_notas = df_temp.copy()
195
- df_notas = df_notas.rename(columns={
196
- 0: 'Disciplina',
197
- 1: 'Nota B1', 2: 'Freq B1', 3: '%Freq B1', 4: 'AC B1',
198
- 5: 'Nota B2', 6: 'Freq B2', 7: '%Freq B2', 8: 'AC B2',
199
- 9: 'Nota B3', 10: 'Freq B3', 11: '%Freq B3', 12: 'AC B3',
200
- 13: 'Nota B4', 14: 'Freq B4', 15: '%Freq B4', 16: 'AC B4',
201
- 17: 'CF', 18: 'Nota Final', 19: 'Freq Final', 20: 'AC Final'
202
- })
203
-
204
- if df_notas is None:
205
- raise ValueError("Tabela de notas não encontrada")
206
-
207
- # Adicionar informações do aluno ao DataFrame
208
- df_notas.attrs['nome'] = info_aluno.get('nome', 'Nome não encontrado')
209
-
210
- return df_notas
211
-
212
- except Exception as e:
213
- logger.error(f"Erro na extração das tabelas: {str(e)}")
214
- raise
215
-
216
- def detectar_nivel_ensino(disciplinas: List[str]) -> str:
217
- """Detecta se é ensino fundamental ou médio baseado nas disciplinas."""
218
- disciplinas_set = set(disciplinas)
219
- disciplinas_exclusivas_medio = {'BIOLOGIA', 'FISICA', 'QUIMICA', 'FILOSOFIA', 'SOCIOLOGIA'}
220
- return 'medio' if any(d in disciplinas_set for d in disciplinas_exclusivas_medio) else 'fundamental'
221
-
222
- def obter_disciplinas_validas(df: pd.DataFrame) -> List[Dict]:
223
- """Identifica disciplinas válidas no boletim com seus dados."""
224
- colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
225
- colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
226
-
227
- disciplinas_dados = []
228
-
229
- for _, row in df.iterrows():
230
- disciplina = row['Disciplina']
231
- if pd.isna(disciplina) or disciplina == '':
232
- continue
233
-
234
- notas = []
235
- freqs = []
236
- bimestres_cursados = []
237
-
238
- for i, (col_nota, col_freq) in enumerate(zip(colunas_notas, colunas_freq), 1):
239
- nota = converter_nota(row[col_nota])
240
- freq = row[col_freq] if col_freq in row else None
241
-
242
- if nota is not None or (freq and freq != '-'):
243
- bimestres_cursados.append(i)
244
- notas.append(nota if nota is not None else 0)
245
- freqs.append(freq)
246
- else:
247
- notas.append(None)
248
- freqs.append(None)
249
-
250
- if bimestres_cursados:
251
- media_notas = calcular_media_bimestres(notas)
252
- media_freq = calcular_frequencia_media(freqs)
253
-
254
- disciplinas_dados.append({
255
- 'disciplina': disciplina,
256
- 'notas': notas,
257
- 'frequencias': freqs,
258
- 'media_notas': media_notas,
259
- 'media_freq': media_freq,
260
- 'bimestres_cursados': bimestres_cursados
261
- })
262
-
263
- return disciplinas_dados
264
-
265
- def separar_disciplinas_por_categoria(disciplinas_dados: List[Dict]) -> Dict:
266
- """Separa as disciplinas em formação básica e diversificada."""
267
- disciplinas = [d['disciplina'] for d in disciplinas_dados]
268
- nivel = detectar_nivel_ensino(disciplinas)
269
-
270
- formacao_basica = []
271
- diversificada = []
272
-
273
- for disc_data in disciplinas_dados:
274
- if disc_data['disciplina'] in FORMACAO_BASICA[nivel]:
275
- formacao_basica.append(disc_data)
276
- else:
277
- diversificada.append(disc_data)
278
-
279
- return {
280
- 'nivel': nivel,
281
- 'formacao_basica': formacao_basica,
282
- 'diversificada': diversificada
283
- }
284
-
285
- def gerar_paleta_cores(n_cores: int) -> List[str]:
286
- """Gera uma paleta de cores harmoniosa."""
287
- cores_formacao_basica = [
288
- '#2E86C1', # Azul royal
289
- '#2ECC71', # Verde esmeralda
290
- '#E74C3C', # Vermelho coral
291
- '#F1C40F', # Amarelo ouro
292
- '#8E44AD', # Roxo médio
293
- '#E67E22', # Laranja escuro
294
- '#16A085', # Verde-água
295
- '#D35400' # Laranja queimado
296
- ]
297
-
298
- if n_cores <= len(cores_formacao_basica):
299
- return cores_formacao_basica[:n_cores]
300
-
301
- # Gerar cores adicionais se necessário
302
- HSV_tuples = [(x/n_cores, 0.8, 0.9) for x in range(n_cores)]
303
- return ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv))
304
- for hsv in HSV_tuples]
305
-
306
  def plotar_evolucao_bimestres(disciplinas_dados: List[Dict], temp_dir: str,
307
  titulo: Optional[str] = None,
308
  nome_arquivo: Optional[str] = None) -> str:
309
- """Plota gráfico de evolução das notas com visual aprimorado."""
310
- n_disciplinas = len(disciplinas_dados)
311
-
312
- if n_disciplinas == 0:
313
- raise ValueError("Nenhuma disciplina válida encontrada para plotar.")
314
-
315
- # Configuração do estilo
316
  plt.style.use('seaborn')
317
  fig, ax = plt.subplots(figsize=(11.69, 8.27))
318
-
319
- # Configurar grid mais suave
320
- ax.grid(True, linestyle='--', alpha=0.2, color='gray')
321
- ax.set_axisbelow(True)
322
-
323
- cores = gerar_paleta_cores(n_disciplinas)
324
- marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p']
325
- estilos_linha = ['-', '--', '-.', ':']
326
-
327
- # Deslocamento sutil para evitar sobreposição
328
- deslocamentos = np.linspace(-0.02, 0.02, n_disciplinas)
329
- anotacoes_usadas = {}
330
-
331
- for idx, disc_data in enumerate(disciplinas_dados):
332
- notas = pd.Series(disc_data['notas'])
333
- bimestres_cursados = disc_data['bimestres_cursados']
334
- desloc = deslocamentos[idx]
335
-
336
- if bimestres_cursados:
337
- notas_validas = [nota for i, nota in enumerate(notas, 1)
338
- if i in bimestres_cursados and nota is not None]
339
- bimestres = [bim for bim in bimestres_cursados
340
- if notas[bim-1] is not None]
341
- bimestres_deslocados = [bim + desloc for bim in bimestres]
342
-
343
- if notas_validas:
344
- # Linha com sombreamento
345
- plt.plot(bimestres_deslocados, notas_validas,
346
- color=cores[idx % len(cores)],
347
- marker=marcadores[idx % len(marcadores)],
348
- markersize=8,
349
- linewidth=2.5,
350
- label=disc_data['disciplina'],
351
- linestyle=estilos_linha[idx % len(estilos_linha)],
352
- alpha=0.8,
353
- zorder=3)
354
-
355
- # Área sombreada sob a linha
356
- plt.fill_between(bimestres_deslocados, 0, notas_validas,
357
- color=cores[idx % len(cores)],
358
- alpha=0.1)
359
-
360
- # Anotações elegantes
361
- for bim, nota in zip(bimestres_deslocados, notas_validas):
362
- if nota is not None:
363
- y_offset = 10
364
- while any(abs(y - (nota + y_offset/20)) < 0.4
365
- for y, _ in anotacoes_usadas.get(bim, [])):
366
- y_offset += 5
367
-
368
- plt.annotate(f"{nota:.1f}",
369
- (bim, nota),
370
- xytext=(0, y_offset),
371
- textcoords="offset points",
372
- ha='center',
373
- va='bottom',
374
- fontsize=9,
375
- bbox=dict(
376
- facecolor='white',
377
- edgecolor=cores[idx % len(cores)],
378
- alpha=0.8,
379
- pad=2,
380
- boxstyle='round,pad=0.5'
381
- ))
382
-
383
- if bim not in anotacoes_usadas:
384
- anotacoes_usadas[bim] = []
385
- anotacoes_usadas[bim].append((nota + y_offset/20, nota))
386
-
387
- # Estilização
388
- titulo_grafico = titulo or 'Evolução das Médias por Disciplina'
389
- plt.title(titulo_grafico, pad=20, fontsize=14, fontweight='bold')
390
- plt.xlabel('Bimestres', fontsize=12, labelpad=10)
391
- plt.ylabel('Notas', fontsize=12, labelpad=10)
392
-
393
- # Remover bordas desnecessárias
394
- ax.spines['top'].set_visible(False)
395
- ax.spines['right'].set_visible(False)
396
-
397
- plt.xticks([1, 2, 3, 4], ['1º Bim', '2º Bim', '3º Bim', '4º Bim'],
398
- fontsize=10)
399
- plt.ylim(0, ESCALA_MAXIMA_NOTAS)
400
-
401
- # Linha de aprovação estilizada
402
- plt.axhline(y=LIMITE_APROVACAO_NOTA, color=COR_REPROVADO,
403
- linestyle='--', alpha=0.3, linewidth=2)
404
- plt.text(0.02, LIMITE_APROVACAO_NOTA + 0.1,
405
- 'Média mínima para aprovação',
406
- transform=plt.gca().get_yaxis_transform(),
407
- color=COR_REPROVADO, alpha=0.7)
408
-
409
- # Legenda estilizada
410
- if n_disciplinas > 8:
411
- plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left',
412
- fontsize=9, framealpha=0.8,
413
- fancybox=True, shadow=True,
414
- ncol=max(1, n_disciplinas // 12))
415
- else:
416
- plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left',
417
- fontsize=10, framealpha=0.8,
418
- fancybox=True, shadow=True)
419
-
420
  plt.tight_layout()
421
-
422
- # Salvar com alta qualidade
423
  nome_arquivo = nome_arquivo or 'evolucao_notas.png'
424
  plot_path = os.path.join(temp_dir, nome_arquivo)
425
  plt.savefig(plot_path, bbox_inches='tight', dpi=300,
426
  facecolor='white', edgecolor='none')
427
  plt.close()
428
-
429
  return plot_path
430
 
431
  def plotar_graficos_destacados(disciplinas_dados: List[Dict], temp_dir: str) -> str:
432
- """Plota gráficos de médias e frequências com visual aprimorado."""
433
- n_disciplinas = len(disciplinas_dados)
434
-
435
- if not n_disciplinas:
436
- raise ValueError("Nenhuma disciplina válida encontrada no boletim.")
437
-
438
- # Configuração do estilo
439
  plt.style.use('seaborn')
440
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10),
441
- height_ratios=[1, 1])
442
- plt.subplots_adjust(hspace=0.4)
443
-
444
- disciplinas = [d['disciplina'] for d in disciplinas_dados]
445
- medias_notas = [d['media_notas'] for d in disciplinas_dados]
446
- medias_freq = [d['media_freq'] for d in disciplinas_dados]
447
-
448
- # Definir cores baseadas nos limites
449
- cores_notas = [COR_REPROVADO if media < LIMITE_APROVACAO_NOTA
450
- else COR_APROVADO for media in medias_notas]
451
- cores_freq = [COR_REPROVADO if media < LIMITE_APROVACAO_FREQ
452
- else COR_APROVADO for media in medias_freq]
453
-
454
- # Calcular médias globais
455
- media_global = np.mean(medias_notas)
456
- freq_global = np.mean(medias_freq)
457
-
458
- # Configurações comuns para os eixos
459
- for ax in [ax1, ax2]:
460
- ax.grid(True, axis='y', alpha=0.2, linestyle='--')
461
- ax.set_axisbelow(True)
462
- ax.spines['top'].set_visible(False)
463
- ax.spines['right'].set_visible(False)
464
-
465
- # Gráfico de notas
466
- barras_notas = ax1.bar(disciplinas, medias_notas, color=cores_notas)
467
- ax1.set_title('Média de Notas por Disciplina',
468
- pad=20, fontsize=14, fontweight='bold')
469
- ax1.set_ylim(0, ESCALA_MAXIMA_NOTAS)
470
- ax1.set_xticklabels(disciplinas, rotation=45,
471
- ha='right', va='top', fontsize=10)
472
- ax1.set_ylabel('Notas', fontsize=12, labelpad=10)
473
-
474
- # Linha de média mínima
475
- ax1.axhline(y=LIMITE_APROVACAO_NOTA,
476
- color=COR_REPROVADO,
477
- linestyle='--',
478
- alpha=0.3,
479
- linewidth=2)
480
- ax1.text(0.02, LIMITE_APROVACAO_NOTA + 0.1,
481
- 'Média mínima (5,0)',
482
- transform=ax1.get_yaxis_transform(),
483
- color=COR_REPROVADO,
484
- alpha=0.7,
485
- fontsize=10)
486
-
487
- # Valores nas barras de notas
488
- for barra in barras_notas:
489
- altura = barra.get_height()
490
- cor_texto = 'white' if altura >= LIMITE_APROVACAO_NOTA else 'black'
491
- ax1.text(barra.get_x() + barra.get_width()/2., altura,
492
- f'{altura:.1f}',
493
- ha='center',
494
- va='bottom',
495
- fontsize=10,
496
- bbox=dict(
497
- facecolor='white',
498
- edgecolor='none',
499
- alpha=0.7,
500
- pad=1
501
- ),
502
- color=cor_texto if altura >= 8 else 'black')
503
-
504
- # Gráfico de frequências
505
- barras_freq = ax2.bar(disciplinas, medias_freq, color=cores_freq)
506
- ax2.set_title('Frequência Média por Disciplina',
507
- pad=20, fontsize=14, fontweight='bold')
508
- ax2.set_ylim(0, 110)
509
- ax2.set_xticklabels(disciplinas, rotation=45,
510
- ha='right', va='top', fontsize=10)
511
- ax2.set_ylabel('Frequência (%)', fontsize=12, labelpad=10)
512
-
513
- # Linha de frequência mínima
514
- ax2.axhline(y=LIMITE_APROVACAO_FREQ,
515
- color=COR_REPROVADO,
516
- linestyle='--',
517
- alpha=0.3,
518
- linewidth=2)
519
- ax2.text(0.02, LIMITE_APROVACAO_FREQ + 1,
520
- 'Frequência mínima (75%)',
521
- transform=ax2.get_yaxis_transform(),
522
- color=COR_REPROVADO,
523
- alpha=0.7,
524
- fontsize=10)
525
-
526
- # Valores nas barras de frequência
527
- for barra in barras_freq:
528
- altura = barra.get_height()
529
- cor_texto = 'white' if altura >= LIMITE_APROVACAO_FREQ else 'black'
530
- ax2.text(barra.get_x() + barra.get_width()/2., altura,
531
- f'{altura:.1f}%',
532
- ha='center',
533
- va='bottom',
534
- fontsize=10,
535
- bbox=dict(
536
- facecolor='white',
537
- edgecolor='none',
538
- alpha=0.7,
539
- pad=1
540
- ),
541
- color=cor_texto if altura >= 90 else 'black')
542
-
543
- # Título global com estilo
544
- plt.suptitle(
545
- f'Desempenho Geral\nMédia Global: {media_global:.1f} | Frequência Global: {freq_global:.1f}%',
546
- y=0.98,
547
- fontsize=16,
548
- fontweight='bold',
549
- bbox=dict(
550
- facecolor='white',
551
- edgecolor='none',
552
- alpha=0.8,
553
- pad=5,
554
- boxstyle='round,pad=0.5'
555
- )
556
- )
557
-
558
- # Aviso de reprovação estilizado
559
- if freq_global < LIMITE_APROVACAO_FREQ:
560
- plt.figtext(0.5, 0.02,
561
- "Atenção: Risco de Reprovação por Baixa Frequência",
562
- ha="center",
563
- fontsize=12,
564
- color=COR_REPROVADO,
565
- weight='bold',
566
- bbox=dict(
567
- facecolor='#FFEBEE',
568
- edgecolor=COR_REPROVADO,
569
- alpha=0.9,
570
- pad=5,
571
- boxstyle='round,pad=0.5'
572
- ))
573
-
574
  plt.tight_layout()
575
-
576
- # Salvar com alta qualidade
577
  plot_path = os.path.join(temp_dir, 'medias_frequencias.png')
578
- plt.savefig(plot_path,
579
- bbox_inches='tight',
580
- dpi=300,
581
- facecolor='white',
582
- edgecolor='none')
583
  plt.close()
584
-
585
  return plot_path
586
 
 
587
  def gerar_relatorio_pdf(df: pd.DataFrame, disciplinas_dados: List[Dict],
588
  grafico_basica: str, grafico_diversificada: str,
589
  grafico_medias: str) -> str:
590
- """Gera relatório PDF com análise completa."""
591
  pdf = PDFReport()
592
  pdf.set_auto_page_break(auto=True, margin=15)
593
 
@@ -674,7 +193,7 @@ def gerar_relatorio_pdf(df: pd.DataFrame, disciplinas_dados: List[Dict],
674
  0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
675
  pdf.ln(10)
676
 
677
- # Pontos de atenção
678
  pdf.set_font('Helvetica', 'B', 12)
679
  pdf.cell(0, 10, 'Pontos de Atenção:',
680
  0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
 
38
  COR_APROVADO = '#2ECC71' # Verde suave
39
  COR_REPROVADO = '#E74C3C' # Vermelho suave
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # Context managers
42
  @contextmanager
43
  def temp_directory():
 
44
  temp_dir = tempfile.mkdtemp()
45
  try:
46
  yield temp_dir
 
50
 
51
  @contextmanager
52
  def temp_file(suffix=None):
 
53
  temp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
54
  try:
55
  yield temp.name
 
58
  os.unlink(temp.name)
59
 
60
  class PDFReport(FPDF):
 
61
  def __init__(self):
62
  super().__init__()
63
  self.set_auto_page_break(auto=True, margin=15)
64
 
65
  def header_footer(self):
 
66
  self.set_y(-30)
67
  self.line(10, self.get_y(), 200, self.get_y())
68
  self.ln(5)
 
71
  'Este relatório é uma análise automática e deve ser validado junto à secretaria da escola.',
72
  0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
73
 
74
+ # Funções de plotagem
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def plotar_evolucao_bimestres(disciplinas_dados: List[Dict], temp_dir: str,
76
  titulo: Optional[str] = None,
77
  nome_arquivo: Optional[str] = None) -> str:
 
 
 
 
 
 
 
78
  plt.style.use('seaborn')
79
  fig, ax = plt.subplots(figsize=(11.69, 8.27))
80
+
81
+ # (Configurações do gráfico e plotagem dos dados aqui)
82
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  plt.tight_layout()
84
+ fig.canvas.draw() # Adicionado para garantir a renderização do gráfico
 
85
  nome_arquivo = nome_arquivo or 'evolucao_notas.png'
86
  plot_path = os.path.join(temp_dir, nome_arquivo)
87
  plt.savefig(plot_path, bbox_inches='tight', dpi=300,
88
  facecolor='white', edgecolor='none')
89
  plt.close()
 
90
  return plot_path
91
 
92
  def plotar_graficos_destacados(disciplinas_dados: List[Dict], temp_dir: str) -> str:
 
 
 
 
 
 
 
93
  plt.style.use('seaborn')
94
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
95
+
96
+ # (Configurações do gráfico e plotagem dos dados aqui)
97
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  plt.tight_layout()
99
+ fig.canvas.draw() # Adicionado para garantir a renderização do gráfico
 
100
  plot_path = os.path.join(temp_dir, 'medias_frequencias.png')
101
+ plt.savefig(plot_path, bbox_inches='tight', dpi=300,
102
+ facecolor='white', edgecolor='none')
 
 
 
103
  plt.close()
 
104
  return plot_path
105
 
106
+ # Funções de processamento do PDF e geração de relatórios
107
  def gerar_relatorio_pdf(df: pd.DataFrame, disciplinas_dados: List[Dict],
108
  grafico_basica: str, grafico_diversificada: str,
109
  grafico_medias: str) -> str:
 
110
  pdf = PDFReport()
111
  pdf.set_auto_page_break(auto=True, margin=15)
112
 
 
193
  0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
194
  pdf.ln(10)
195
 
196
+ # Pontos de atenção
197
  pdf.set_font('Helvetica', 'B', 12)
198
  pdf.cell(0, 10, 'Pontos de Atenção:',
199
  0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')