entropy25 commited on
Commit
2aecc9c
Β·
verified Β·
1 Parent(s): b028978

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -274
app.py CHANGED
@@ -12,11 +12,6 @@ from reportlab.lib.pagesizes import letter, A4
12
  from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
13
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
14
  from reportlab.lib.units import inch
15
- from reportlab.graphics.shapes import Drawing
16
- from reportlab.graphics.charts.linecharts import HorizontalLineChart
17
- from reportlab.graphics.charts.piecharts import Pie
18
- from reportlab.graphics.charts.barcharts import VerticalBarChart
19
- from reportlab.graphics import renderPDF
20
  import plotly.io as pio
21
  import tempfile
22
  import os
@@ -24,14 +19,14 @@ import os
24
  # Design System Configuration
25
  DESIGN_SYSTEM = {
26
  'colors': {
27
- 'primary': '#1E40AF', # Blue
28
- 'secondary': '#059669', # Green
29
- 'accent': '#DC2626', # Red
30
- 'warning': '#D97706', # Orange
31
- 'success': '#10B981', # Emerald
32
- 'background': '#F8FAFC', # Light gray
33
- 'text': '#1F2937', # Dark gray
34
- 'border': '#E5E7EB' # Light border
35
  },
36
  'fonts': {
37
  'title': 'font-family: "Inter", sans-serif; font-weight: 700;',
@@ -48,7 +43,7 @@ st.set_page_config(
48
  initial_sidebar_state="expanded"
49
  )
50
 
51
- # Custom CSS for design system
52
  def load_css():
53
  st.markdown(f"""
54
  <style>
@@ -86,11 +81,6 @@ def load_css():
86
  transition: transform 0.2s ease;
87
  }}
88
 
89
- .metric-card:hover {{
90
- transform: translateY(-2px);
91
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
92
- }}
93
-
94
  .section-header {{
95
  {DESIGN_SYSTEM['fonts']['subtitle']}
96
  color: {DESIGN_SYSTEM['colors']['text']};
@@ -124,11 +114,6 @@ def load_css():
124
  color: {DESIGN_SYSTEM['colors']['warning']};
125
  }}
126
 
127
- .stSelectbox > div > div {{
128
- border-radius: 8px;
129
- border: 1px solid {DESIGN_SYSTEM['colors']['border']};
130
- }}
131
-
132
  .stButton > button {{
133
  background: {DESIGN_SYSTEM['colors']['primary']};
134
  color: white;
@@ -138,11 +123,6 @@ def load_css():
138
  font-weight: 500;
139
  transition: all 0.2s ease;
140
  }}
141
-
142
- .stButton > button:hover {{
143
- background: {DESIGN_SYSTEM['colors']['secondary']};
144
- transform: translateY(-1px);
145
- }}
146
  </style>
147
  """, unsafe_allow_html=True)
148
 
@@ -341,66 +321,66 @@ def query_ai(model, stats, question, df=None):
341
  except:
342
  return "Error getting AI response"
343
 
344
- def create_production_pie_chart():
345
- """Create production distribution pie chart for PDF"""
346
- drawing = Drawing(400, 200)
347
- pie = Pie()
348
- pie.x = 50
349
- pie.y = 50
350
- pie.width = 120
351
- pie.height = 120
352
-
353
- # Sample data - will be replaced with real data
354
- pie.data = [60, 30, 10]
355
- pie.labels = ['Liquid', 'Solid', 'Waste Water']
356
- pie.slices.strokeWidth = 0.5
357
- pie.slices[0].fillColor = colors.blue
358
- pie.slices[1].fillColor = colors.green
359
- pie.slices[2].fillColor = colors.orange
360
-
361
- drawing.add(pie)
362
- return drawing
363
-
364
- def create_trend_chart():
365
- """Create production trend chart for PDF"""
366
- drawing = Drawing(400, 200)
367
- chart = HorizontalLineChart()
368
- chart.x = 50
369
- chart.y = 50
370
- chart.height = 120
371
- chart.width = 300
372
- chart.data = [
373
- [100, 120, 140, 110, 160, 150, 180],
374
- ]
375
- chart.lines[0].strokeColor = colors.blue
376
- chart.lines[0].strokeWidth = 2
377
-
378
- drawing.add(chart)
379
- return drawing
380
-
381
  def save_plotly_as_image(fig, filename):
382
- """Convert Plotly figure to image for PDF"""
383
  try:
384
- # Create temp directory if it doesn't exist
385
  temp_dir = tempfile.gettempdir()
386
  filepath = os.path.join(temp_dir, filename)
387
 
388
- # Save as PNG
389
- pio.write_image(fig, filepath, format='png', width=800, height=400)
 
 
 
 
 
 
 
390
  return filepath
391
  except Exception as e:
392
- print(f"Error saving chart: {e}")
393
  return None
394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  def create_enhanced_pdf_report(df, stats, outliers):
396
- """Generate enhanced PDF report with charts and detailed analysis"""
397
  buffer = io.BytesIO()
398
  doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50)
399
 
400
  elements = []
401
-
402
- # Define custom styles
403
  styles = getSampleStyleSheet()
 
 
404
  title_style = ParagraphStyle(
405
  'CustomTitle',
406
  parent=styles['Heading1'],
@@ -415,29 +395,26 @@ def create_enhanced_pdf_report(df, stats, outliers):
415
  parent=styles['Heading2'],
416
  fontSize=16,
417
  spaceAfter=20,
418
- textColor=colors.darkblue,
419
- borderWidth=1,
420
- borderColor=colors.darkblue,
421
- borderPadding=10
422
  )
423
 
424
- # Cover Page
425
  elements.append(Spacer(1, 100))
426
- elements.append(Paragraph("🏭 PRODUCTION MONITOR", title_style))
427
- elements.append(Paragraph("Comprehensive Production Analysis Report", styles['Heading3']))
428
  elements.append(Spacer(1, 50))
429
 
430
- # Company info
431
- company_info = f"""
432
  <para alignment="center">
433
  <b>Nilsen Service & Consulting AS</b><br/>
434
  Production Analytics Division<br/><br/>
435
  <b>Report Period:</b> {df['date'].min().strftime('%B %d, %Y')} - {df['date'].max().strftime('%B %d, %Y')}<br/>
436
  <b>Generated:</b> {datetime.now().strftime('%B %d, %Y at %H:%M')}<br/>
437
- <b>Total Records Analyzed:</b> {len(df):,}
438
  </para>
439
  """
440
- elements.append(Paragraph(company_info, styles['Normal']))
441
  elements.append(PageBreak())
442
 
443
  # Executive Summary
@@ -449,214 +426,106 @@ def create_enhanced_pdf_report(df, stats, outliers):
449
 
450
  exec_summary = f"""
451
  <para>
452
- This report presents a comprehensive analysis of production data spanning <b>{work_days} working days</b>.
453
- Our production facilities achieved a total output of <b>{total_production:,.0f} kg</b> with an average
454
  daily production of <b>{daily_avg:,.0f} kg</b>.
455
  <br/><br/>
456
  <b>Key Highlights:</b><br/>
457
- β€’ Total production volume: {total_production:,.0f} kg<br/>
458
- β€’ Average daily output: {daily_avg:,.0f} kg<br/>
459
- β€’ Production efficiency: {(len([info for info in outliers.values() if info['count'] == 0]) / len(outliers)) * 100:.0f}% materials within normal range<br/>
460
- β€’ Data quality: {len(df):,} records processed with high accuracy
461
  </para>
462
  """
463
  elements.append(Paragraph(exec_summary, styles['Normal']))
464
  elements.append(Spacer(1, 20))
465
 
466
- # Production Overview Table
467
- elements.append(Paragraph("Production Breakdown by Material", styles['Heading3']))
468
 
469
- summary_data = [['Material Type', 'Total Output (kg)', 'Market Share (%)', 'Daily Average (kg)', 'Performance Rating']]
470
 
471
  for material, info in stats.items():
472
  if material != '_total_':
473
- # Calculate performance rating
474
- outlier_count = outliers.get(material, {}).get('count', 0)
475
- if outlier_count == 0:
476
- rating = "⭐⭐⭐⭐⭐ Excellent"
477
- elif outlier_count <= 2:
478
- rating = "⭐⭐⭐⭐ Good"
479
- elif outlier_count <= 5:
480
- rating = "⭐⭐⭐ Fair"
481
- else:
482
- rating = "⭐⭐ Needs Attention"
483
-
484
  summary_data.append([
485
  material.replace('_', ' ').title(),
486
  f"{info['total']:,.0f}",
487
  f"{info['percentage']:.1f}%",
488
- f"{info['daily_avg']:,.0f}",
489
- rating
490
  ])
491
 
492
- # Add total row
493
- total_info = stats['_total_']
494
- summary_data.append([
495
- 'TOTAL PRODUCTION',
496
- f"{total_info['total']:,.0f}",
497
- '100.0%',
498
- f"{total_info['daily_avg']:,.0f}",
499
- 'πŸ“Š Combined'
500
- ])
501
-
502
- summary_table = Table(summary_data, colWidths=[2*inch, 1.5*inch, 1*inch, 1.5*inch, 1.5*inch])
503
  summary_table.setStyle(TableStyle([
504
  ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
505
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
506
  ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
507
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
508
- ('FONTSIZE', (0, 0), (-1, 0), 12),
509
- ('BOTTOMPADDING', (0, 0), (-1, 0), 15),
510
- ('BACKGROUND', (0, -1), (-1, -1), colors.lightblue),
511
- ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
512
  ('GRID', (0, 0), (-1, -1), 1, colors.black),
513
- ('ROWBACKGROUNDS', (0, 1), (-1, -2), [colors.white, colors.lightgrey]),
514
  ]))
515
 
516
  elements.append(summary_table)
517
- elements.append(Spacer(1, 30))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
- # Add simple charts
520
- elements.append(Paragraph("Production Distribution", styles['Heading3']))
 
 
521
 
522
- # Create a simple pie chart
523
- pie_chart = create_production_pie_chart()
524
- elements.append(pie_chart)
525
- elements.append(Spacer(1, 30))
526
 
527
- # Quality Analysis Section
528
  elements.append(PageBreak())
529
  elements.append(Paragraph("Quality Control Analysis", subtitle_style))
530
 
531
- quality_intro = """
532
- <para>
533
- Our quality control systems continuously monitor production values to identify anomalies
534
- and ensure consistent output. The following analysis uses statistical methods (IQR-based
535
- outlier detection) to identify production values that deviate significantly from normal patterns.
536
- </para>
537
- """
538
- elements.append(Paragraph(quality_intro, styles['Normal']))
539
- elements.append(Spacer(1, 20))
540
-
541
- # Detailed Quality Table
542
- quality_data = [['Material', 'Total Outliers', 'Normal Range (kg)', 'Outlier Rate (%)', 'Status', 'Action Required']]
543
 
544
  for material, info in outliers.items():
545
- outlier_rate = (info['count'] / stats[material]['records']) * 100 if stats[material]['records'] > 0 else 0
546
-
547
  if info['count'] == 0:
548
- status = "βœ… EXCELLENT"
549
- action = "Continue monitoring"
550
- elif info['count'] <= 2:
551
- status = "🟑 ACCEPTABLE"
552
- action = "Routine check"
553
- elif info['count'] <= 5:
554
- status = "🟠 ATTENTION"
555
- action = "Review procedures"
556
  else:
557
- status = "πŸ”΄ CRITICAL"
558
- action = "Immediate investigation"
559
 
560
  quality_data.append([
561
  material.replace('_', ' ').title(),
562
  str(info['count']),
563
  info['range'],
564
- f"{outlier_rate:.1f}%",
565
- status,
566
- action
567
  ])
568
 
569
- quality_table = Table(quality_data, colWidths=[1.3*inch, 0.8*inch, 1.2*inch, 0.8*inch, 1*inch, 1.4*inch])
570
  quality_table.setStyle(TableStyle([
571
  ('BACKGROUND', (0, 0), (-1, 0), colors.darkred),
572
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
573
  ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
574
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
575
- ('FONTSIZE', (0, 0), (-1, 0), 10),
576
- ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
577
  ('GRID', (0, 0), (-1, -1), 1, colors.black),
578
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
579
- ('FONTSIZE', (0, 1), (-1, -1), 9),
580
  ]))
581
 
582
  elements.append(quality_table)
583
- elements.append(Spacer(1, 30))
584
-
585
- # Statistical Analysis
586
- elements.append(Paragraph("Statistical Performance Metrics", styles['Heading3']))
587
-
588
- # Calculate detailed statistics
589
- daily_totals = df.groupby('date')['weight_kg'].sum()
590
- stats_desc = daily_totals.describe()
591
-
592
- # Create statistics table
593
- stats_data = [
594
- ['Metric', 'Value (kg)', 'Interpretation'],
595
- ['Mean Daily Production', f"{stats_desc['mean']:,.0f}", 'Average daily output'],
596
- ['Median Daily Production', f"{stats_desc['50%']:,.0f}", 'Typical daily output'],
597
- ['Standard Deviation', f"{stats_desc['std']:,.0f}", 'Production variability'],
598
- ['Minimum Daily Output', f"{stats_desc['min']:,.0f}", 'Lowest single day'],
599
- ['Maximum Daily Output', f"{stats_desc['max']:,.0f}", 'Highest single day'],
600
- ['25th Percentile', f"{stats_desc['25%']:,.0f}", 'Lower quartile'],
601
- ['75th Percentile', f"{stats_desc['75%']:,.0f}", 'Upper quartile'],
602
- ]
603
-
604
- # Add coefficient of variation
605
- cv = (stats_desc['std'] / stats_desc['mean']) * 100
606
- stats_data.append(['Coefficient of Variation', f"{cv:.1f}%", 'Production consistency'])
607
-
608
- stats_table = Table(stats_data, colWidths=[2.5*inch, 1.5*inch, 2.5*inch])
609
- stats_table.setStyle(TableStyle([
610
- ('BACKGROUND', (0, 0), (-1, 0), colors.darkgreen),
611
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
612
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
613
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
614
- ('GRID', (0, 0), (-1, -1), 1, colors.black),
615
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
616
- ]))
617
-
618
- elements.append(stats_table)
619
- elements.append(Spacer(1, 30))
620
-
621
- # Recommendations Section
622
- elements.append(PageBreak())
623
- elements.append(Paragraph("Recommendations & Action Items", subtitle_style))
624
-
625
- # Generate intelligent recommendations
626
- recommendations = []
627
-
628
- # Check for high-variability materials
629
- high_var_materials = [mat for mat, info in outliers.items() if info['count'] > 5]
630
- if high_var_materials:
631
- recommendations.append(f"πŸ”§ <b>Equipment Review:</b> Materials {', '.join(high_var_materials)} show high variability. Consider equipment calibration.")
632
-
633
- # Check for low production days
634
- if stats_desc['min'] < stats_desc['mean'] * 0.7:
635
- recommendations.append(f"πŸ“Š <b>Process Optimization:</b> Minimum daily output ({stats_desc['min']:,.0f} kg) is significantly below average. Investigate bottlenecks.")
636
-
637
- # Check consistency
638
- if cv > 20:
639
- recommendations.append(f"βš–οΈ <b>Consistency Improvement:</b> High production variability (CV: {cv:.1f}%). Implement process standardization.")
640
- else:
641
- recommendations.append(f"βœ… <b>Process Stability:</b> Good production consistency (CV: {cv:.1f}%). Maintain current procedures.")
642
-
643
- # Material-specific recommendations
644
- top_material = max([k for k in stats.keys() if k != '_total_'], key=lambda x: stats[x]['total'])
645
- recommendations.append(f"🎯 <b>Focus Area:</b> {top_material.replace('_', ' ').title()} is your primary material ({stats[top_material]['percentage']:.1f}% of production). Optimize this line for maximum impact.")
646
-
647
- for i, rec in enumerate(recommendations, 1):
648
- elements.append(Paragraph(f"{i}. {rec}", styles['Normal']))
649
- elements.append(Spacer(1, 10))
650
-
651
- # Footer
652
- elements.append(Spacer(1, 50))
653
- footer_text = """
654
- <para alignment="center">
655
- <i>This report was generated automatically by the Production Monitor system.<br/>
656
- For questions or additional analysis, contact the Production Analytics team.</i>
657
- </para>
658
- """
659
- elements.append(Paragraph(footer_text, styles['Normal']))
660
 
661
  # Build PDF
662
  doc.build(elements)
@@ -665,7 +534,6 @@ def create_enhanced_pdf_report(df, stats, outliers):
665
 
666
  def create_csv_export(df, stats):
667
  """Create CSV export of summary data"""
668
-
669
  summary_df = pd.DataFrame([
670
  {
671
  'Material': material.replace('_', ' ').title(),
@@ -678,22 +546,10 @@ def create_csv_export(df, stats):
678
  for material, info in stats.items() if material != '_total_'
679
  ])
680
 
681
- total_info = stats['_total_']
682
- total_row = pd.DataFrame([{
683
- 'Material': 'TOTAL',
684
- 'Total_kg': total_info['total'],
685
- 'Percentage': 100.0,
686
- 'Daily_Average_kg': total_info['daily_avg'],
687
- 'Work_Days': total_info['work_days'],
688
- 'Records_Count': total_info['records']
689
- }])
690
-
691
- summary_df = pd.concat([summary_df, total_row], ignore_index=True)
692
-
693
  return summary_df
694
 
695
  def add_export_section(df, stats, outliers):
696
- """Add export functionality to the main app"""
697
  st.markdown('<div class="section-header">πŸ“„ Export Reports</div>', unsafe_allow_html=True)
698
 
699
  col1, col2, col3 = st.columns(3)
@@ -701,7 +557,7 @@ def add_export_section(df, stats, outliers):
701
  with col1:
702
  if st.button("πŸ“Š Download PDF Report", type="primary"):
703
  try:
704
- with st.spinner("Generating PDF..."):
705
  pdf_buffer = create_enhanced_pdf_report(df, stats, outliers)
706
 
707
  st.download_button(
@@ -710,27 +566,22 @@ def add_export_section(df, stats, outliers):
710
  file_name=f"production_report_{datetime.now().strftime('%Y%m%d')}.pdf",
711
  mime="application/pdf"
712
  )
713
- st.success("PDF report ready for download!")
714
 
715
  except Exception as e:
716
  st.error(f"PDF generation failed: {str(e)}")
717
 
718
  with col2:
719
  if st.button("πŸ“ˆ Download CSV Summary"):
720
- try:
721
- csv_data = create_csv_export(df, stats)
722
- csv_string = csv_data.to_csv(index=False)
723
-
724
- st.download_button(
725
- label="πŸ’Ύ Download CSV",
726
- data=csv_string,
727
- file_name=f"production_summary_{datetime.now().strftime('%Y%m%d')}.csv",
728
- mime="text/csv"
729
- )
730
- st.success("CSV summary ready for download!")
731
-
732
- except Exception as e:
733
- st.error(f"CSV generation failed: {str(e)}")
734
 
735
  with col3:
736
  if st.button("πŸ“‹ Download Raw Data"):
@@ -739,14 +590,14 @@ def add_export_section(df, stats, outliers):
739
  st.download_button(
740
  label="πŸ’Ύ Download Raw CSV",
741
  data=csv_string,
742
- file_name=f"raw_production_data_{datetime.now().strftime('%Y%m%d')}.csv",
743
  mime="text/csv"
744
  )
745
 
746
  def main():
747
  load_css()
748
 
749
- # Modern header
750
  st.markdown("""
751
  <div class="main-header">
752
  <div class="main-title">🏭 Production Monitor</div>
@@ -764,7 +615,7 @@ def main():
764
  st.markdown("""
765
  **Expected TSV format:**
766
  - `date`: MM/DD/YYYY
767
- - `weight_kg`: Production weight in kg
768
  - `material_type`: Material category
769
  - `shift`: day/night (optional)
770
  """)
@@ -890,7 +741,7 @@ def main():
890
  st.write(f"**A:** {answer}")
891
 
892
  else:
893
- # Platform usage guide
894
  st.markdown('<div class="section-header">πŸ“– How to Use This Platform</div>', unsafe_allow_html=True)
895
 
896
  col1, col2 = st.columns(2)
 
12
  from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
13
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
14
  from reportlab.lib.units import inch
 
 
 
 
 
15
  import plotly.io as pio
16
  import tempfile
17
  import os
 
19
  # Design System Configuration
20
  DESIGN_SYSTEM = {
21
  'colors': {
22
+ 'primary': '#1E40AF',
23
+ 'secondary': '#059669',
24
+ 'accent': '#DC2626',
25
+ 'warning': '#D97706',
26
+ 'success': '#10B981',
27
+ 'background': '#F8FAFC',
28
+ 'text': '#1F2937',
29
+ 'border': '#E5E7EB'
30
  },
31
  'fonts': {
32
  'title': 'font-family: "Inter", sans-serif; font-weight: 700;',
 
43
  initial_sidebar_state="expanded"
44
  )
45
 
46
+ # Custom CSS
47
  def load_css():
48
  st.markdown(f"""
49
  <style>
 
81
  transition: transform 0.2s ease;
82
  }}
83
 
 
 
 
 
 
84
  .section-header {{
85
  {DESIGN_SYSTEM['fonts']['subtitle']}
86
  color: {DESIGN_SYSTEM['colors']['text']};
 
114
  color: {DESIGN_SYSTEM['colors']['warning']};
115
  }}
116
 
 
 
 
 
 
117
  .stButton > button {{
118
  background: {DESIGN_SYSTEM['colors']['primary']};
119
  color: white;
 
123
  font-weight: 500;
124
  transition: all 0.2s ease;
125
  }}
 
 
 
 
 
126
  </style>
127
  """, unsafe_allow_html=True)
128
 
 
321
  except:
322
  return "Error getting AI response"
323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  def save_plotly_as_image(fig, filename):
325
+ """Convert Plotly figure to PNG for PDF"""
326
  try:
 
327
  temp_dir = tempfile.gettempdir()
328
  filepath = os.path.join(temp_dir, filename)
329
 
330
+ # Optimize for PDF
331
+ fig.update_layout(
332
+ font=dict(size=12, family="Arial"),
333
+ plot_bgcolor='white',
334
+ paper_bgcolor='white',
335
+ margin=dict(t=50, b=40, l=40, r=40)
336
+ )
337
+
338
+ pio.write_image(fig, filepath, format='png', width=800, height=400, scale=2)
339
  return filepath
340
  except Exception as e:
341
+ st.error(f"Chart save error: {e}")
342
  return None
343
 
344
+ def create_pdf_charts(df, stats):
345
+ """Generate charts for PDF report"""
346
+ charts = {}
347
+
348
+ # Production distribution pie chart
349
+ materials = [k for k in stats.keys() if k != '_total_']
350
+ values = [stats[mat]['total'] for mat in materials]
351
+ labels = [mat.replace('_', ' ').title() for mat in materials]
352
+
353
+ fig_pie = px.pie(values=values, names=labels, title="Production Distribution by Material")
354
+ charts['pie'] = save_plotly_as_image(fig_pie, "distribution.png")
355
+
356
+ # Daily production trend
357
+ daily_data = df.groupby('date')['weight_kg'].sum().reset_index()
358
+ fig_trend = px.line(daily_data, x='date', y='weight_kg', title="Daily Production Trend")
359
+ charts['trend'] = save_plotly_as_image(fig_trend, "trend.png")
360
+
361
+ # Material comparison bar chart
362
+ fig_bar = px.bar(x=labels, y=values, title="Production by Material Type")
363
+ fig_bar.update_xaxis(title="Material Type")
364
+ fig_bar.update_yaxis(title="Weight (kg)")
365
+ charts['bar'] = save_plotly_as_image(fig_bar, "materials.png")
366
+
367
+ # Shift analysis if available
368
+ if 'shift' in df.columns:
369
+ shift_data = df.groupby('shift')['weight_kg'].sum().reset_index()
370
+ fig_shift = px.pie(shift_data, values='weight_kg', names='shift', title="Production by Shift")
371
+ charts['shift'] = save_plotly_as_image(fig_shift, "shifts.png")
372
+
373
+ return charts
374
+
375
  def create_enhanced_pdf_report(df, stats, outliers):
376
+ """Generate PDF report with Plotly charts only"""
377
  buffer = io.BytesIO()
378
  doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50)
379
 
380
  elements = []
 
 
381
  styles = getSampleStyleSheet()
382
+
383
+ # Custom styles
384
  title_style = ParagraphStyle(
385
  'CustomTitle',
386
  parent=styles['Heading1'],
 
395
  parent=styles['Heading2'],
396
  fontSize=16,
397
  spaceAfter=20,
398
+ textColor=colors.darkblue
 
 
 
399
  )
400
 
401
+ # Cover page
402
  elements.append(Spacer(1, 100))
403
+ elements.append(Paragraph("Production Monitor Report", title_style))
404
+ elements.append(Paragraph("Comprehensive Production Analysis", styles['Heading3']))
405
  elements.append(Spacer(1, 50))
406
 
407
+ # Report info
408
+ report_info = f"""
409
  <para alignment="center">
410
  <b>Nilsen Service & Consulting AS</b><br/>
411
  Production Analytics Division<br/><br/>
412
  <b>Report Period:</b> {df['date'].min().strftime('%B %d, %Y')} - {df['date'].max().strftime('%B %d, %Y')}<br/>
413
  <b>Generated:</b> {datetime.now().strftime('%B %d, %Y at %H:%M')}<br/>
414
+ <b>Total Records:</b> {len(df):,}
415
  </para>
416
  """
417
+ elements.append(Paragraph(report_info, styles['Normal']))
418
  elements.append(PageBreak())
419
 
420
  # Executive Summary
 
426
 
427
  exec_summary = f"""
428
  <para>
429
+ This report analyzes production data spanning <b>{work_days} working days</b>.
430
+ Total output achieved: <b>{total_production:,.0f} kg</b> with an average
431
  daily production of <b>{daily_avg:,.0f} kg</b>.
432
  <br/><br/>
433
  <b>Key Highlights:</b><br/>
434
+ β€’ Total production: {total_production:,.0f} kg<br/>
435
+ β€’ Daily average: {daily_avg:,.0f} kg<br/>
436
+ β€’ Materials tracked: {len([k for k in stats.keys() if k != '_total_'])}<br/>
437
+ β€’ Data quality: {len(df):,} records processed
438
  </para>
439
  """
440
  elements.append(Paragraph(exec_summary, styles['Normal']))
441
  elements.append(Spacer(1, 20))
442
 
443
+ # Production Summary Table
444
+ elements.append(Paragraph("Production Summary", styles['Heading3']))
445
 
446
+ summary_data = [['Material Type', 'Total (kg)', 'Share (%)', 'Daily Avg (kg)']]
447
 
448
  for material, info in stats.items():
449
  if material != '_total_':
 
 
 
 
 
 
 
 
 
 
 
450
  summary_data.append([
451
  material.replace('_', ' ').title(),
452
  f"{info['total']:,.0f}",
453
  f"{info['percentage']:.1f}%",
454
+ f"{info['daily_avg']:,.0f}"
 
455
  ])
456
 
457
+ summary_table = Table(summary_data, colWidths=[2*inch, 1.5*inch, 1*inch, 1.5*inch])
 
 
 
 
 
 
 
 
 
 
458
  summary_table.setStyle(TableStyle([
459
  ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
460
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
461
  ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
462
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
 
 
 
 
463
  ('GRID', (0, 0), (-1, -1), 1, colors.black),
464
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
465
  ]))
466
 
467
  elements.append(summary_table)
468
+ elements.append(PageBreak())
469
+
470
+ # Generate and add charts
471
+ elements.append(Paragraph("Production Analysis Charts", subtitle_style))
472
+
473
+ with st.spinner("Generating charts for PDF..."):
474
+ charts = create_pdf_charts(df, stats)
475
+
476
+ # Add charts to PDF
477
+ if charts['pie'] and os.path.exists(charts['pie']):
478
+ elements.append(Paragraph("Production Distribution", styles['Heading3']))
479
+ elements.append(Image(charts['pie'], width=6*inch, height=3*inch))
480
+ elements.append(Spacer(1, 20))
481
+
482
+ if charts['trend'] and os.path.exists(charts['trend']):
483
+ elements.append(Paragraph("Production Trend", styles['Heading3']))
484
+ elements.append(Image(charts['trend'], width=6*inch, height=3*inch))
485
+ elements.append(Spacer(1, 20))
486
 
487
+ if charts['bar'] and os.path.exists(charts['bar']):
488
+ elements.append(Paragraph("Material Comparison", styles['Heading3']))
489
+ elements.append(Image(charts['bar'], width=6*inch, height=3*inch))
490
+ elements.append(Spacer(1, 20))
491
 
492
+ if 'shift' in charts and charts['shift'] and os.path.exists(charts['shift']):
493
+ elements.append(Paragraph("Shift Analysis", styles['Heading3']))
494
+ elements.append(Image(charts['shift'], width=6*inch, height=3*inch))
495
+ elements.append(Spacer(1, 20))
496
 
497
+ # Quality Analysis
498
  elements.append(PageBreak())
499
  elements.append(Paragraph("Quality Control Analysis", subtitle_style))
500
 
501
+ quality_data = [['Material', 'Outliers', 'Normal Range (kg)', 'Status']]
 
 
 
 
 
 
 
 
 
 
 
502
 
503
  for material, info in outliers.items():
 
 
504
  if info['count'] == 0:
505
+ status = "βœ… GOOD"
506
+ elif info['count'] <= 3:
507
+ status = "⚠️ MONITOR"
 
 
 
 
 
508
  else:
509
+ status = "πŸ”΄ ATTENTION"
 
510
 
511
  quality_data.append([
512
  material.replace('_', ' ').title(),
513
  str(info['count']),
514
  info['range'],
515
+ status
 
 
516
  ])
517
 
518
+ quality_table = Table(quality_data, colWidths=[2*inch, 1*inch, 2*inch, 1.5*inch])
519
  quality_table.setStyle(TableStyle([
520
  ('BACKGROUND', (0, 0), (-1, 0), colors.darkred),
521
  ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
522
  ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
523
  ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
 
 
524
  ('GRID', (0, 0), (-1, -1), 1, colors.black),
525
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
 
526
  ]))
527
 
528
  elements.append(quality_table)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
 
530
  # Build PDF
531
  doc.build(elements)
 
534
 
535
  def create_csv_export(df, stats):
536
  """Create CSV export of summary data"""
 
537
  summary_df = pd.DataFrame([
538
  {
539
  'Material': material.replace('_', ' ').title(),
 
546
  for material, info in stats.items() if material != '_total_'
547
  ])
548
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  return summary_df
550
 
551
  def add_export_section(df, stats, outliers):
552
+ """Add export functionality"""
553
  st.markdown('<div class="section-header">πŸ“„ Export Reports</div>', unsafe_allow_html=True)
554
 
555
  col1, col2, col3 = st.columns(3)
 
557
  with col1:
558
  if st.button("πŸ“Š Download PDF Report", type="primary"):
559
  try:
560
+ with st.spinner("Generating PDF with charts..."):
561
  pdf_buffer = create_enhanced_pdf_report(df, stats, outliers)
562
 
563
  st.download_button(
 
566
  file_name=f"production_report_{datetime.now().strftime('%Y%m%d')}.pdf",
567
  mime="application/pdf"
568
  )
569
+ st.success("PDF report ready!")
570
 
571
  except Exception as e:
572
  st.error(f"PDF generation failed: {str(e)}")
573
 
574
  with col2:
575
  if st.button("πŸ“ˆ Download CSV Summary"):
576
+ csv_data = create_csv_export(df, stats)
577
+ csv_string = csv_data.to_csv(index=False)
578
+
579
+ st.download_button(
580
+ label="πŸ’Ύ Download CSV",
581
+ data=csv_string,
582
+ file_name=f"production_summary_{datetime.now().strftime('%Y%m%d')}.csv",
583
+ mime="text/csv"
584
+ )
 
 
 
 
 
585
 
586
  with col3:
587
  if st.button("πŸ“‹ Download Raw Data"):
 
590
  st.download_button(
591
  label="πŸ’Ύ Download Raw CSV",
592
  data=csv_string,
593
+ file_name=f"raw_data_{datetime.now().strftime('%Y%m%d')}.csv",
594
  mime="text/csv"
595
  )
596
 
597
  def main():
598
  load_css()
599
 
600
+ # Header
601
  st.markdown("""
602
  <div class="main-header">
603
  <div class="main-title">🏭 Production Monitor</div>
 
615
  st.markdown("""
616
  **Expected TSV format:**
617
  - `date`: MM/DD/YYYY
618
+ - `weight_kg`: Production weight
619
  - `material_type`: Material category
620
  - `shift`: day/night (optional)
621
  """)
 
741
  st.write(f"**A:** {answer}")
742
 
743
  else:
744
+ # Usage guide
745
  st.markdown('<div class="section-header">πŸ“– How to Use This Platform</div>', unsafe_allow_html=True)
746
 
747
  col1, col2 = st.columns(2)