SHELLAPANDIANGANHUNGING commited on
Commit
5356abb
·
verified ·
1 Parent(s): 75526ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +316 -133
app.py CHANGED
@@ -563,164 +563,347 @@ st.markdown(f"""
563
  # </div>
564
  # </div>
565
  # """, unsafe_allow_html=True)
566
- import pandas as pd
567
- import plotly.express as px
568
- import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
- # Judul Objective 2 — Center-aligned, elegant box-style (kanan atas kotak)
571
- st.markdown('''
572
- <div style="text-align: center; margin-bottom: 16px;">
573
- <div style="
574
- background-color: #2C2C2C;
575
- color: white;
576
- padding: 10px 16px;
577
- display: inline-block;
578
- border-radius: 6px;
579
- font-weight: 600;
580
- position: relative;
581
- top: 0; right: 0;
582
- ">
583
- OBJECTIVE 2: Hourly Data Capture vs Alarm Count Analysis
584
- </div>
585
- </div>
586
- ''', unsafe_allow_html=True)
587
 
588
- col1, col2 = st.columns(2)
 
 
589
 
590
- # =============== COL 1: Capture Data per Jam per Tyre ===============
591
- with col1:
592
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Data Capture per Hour by Tyre Position</h5>', unsafe_allow_html=True)
593
 
594
- # Hitung jumlah data capture per jam dan per posisi (semua data, bukan hanya alarm)
595
- capture_data = dff.copy()
596
- hourly_capture_counts = capture_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
 
597
 
598
- # Pastikan semua posisi (1,2,3,4) ada
599
- for pos in [1, 2, 3, 4]:
600
- if pos not in hourly_capture_counts.columns:
601
- hourly_capture_counts[pos] = 0
602
 
603
- hourly_capture_counts = hourly_capture_counts[[1, 2, 3, 4]].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
 
605
- # Melt data untuk plotting
606
- hourly_capture_melted = hourly_capture_counts.reset_index().melt(
607
- id_vars='hour',
608
- value_vars=[1, 2, 3, 4],
609
- var_name='Position',
610
- value_name='Capture Count'
611
- )
612
- hourly_capture_melted['Position'] = hourly_capture_melted['Position'].astype(str)
613
-
614
- # Warna sesuai preferensi
615
- color_map = {
616
- '1': '#003DA5', # Dark blue
617
- '2': '#7FA6E8', # Light blue
618
- '3': '#FFB300', # Amber (PLN yellow)
619
- '4': '#FFE082' # Light amber
620
- }
621
 
622
- fig1 = px.line(
623
- hourly_capture_melted,
624
- x='hour',
625
- y='Capture Count',
626
- color='Position',
627
- color_discrete_map=color_map,
628
- title="Data Capture per Hour by Tyre Position",
629
- labels={'Capture Count': 'Number of Records', 'Position': 'Tyre Position'},
630
- line_shape='linear',
631
- template="plotly_white"
632
- )
633
 
634
- fig1.update_layout(
635
- xaxis=dict(
636
- title="Hour of Day",
637
- tickmode='array',
638
- tickvals=list(range(0, 24)),
639
- ticktext=[f"{h:02d}:00" for h in range(24)],
640
- tickangle=45
641
- ),
642
- yaxis=dict(title="Number of Records"),
643
- legend_title_text='Tyre Position',
644
- margin=dict(t=40, b=40, l=40, r=20),
645
- title_x=0.5
646
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
 
648
- st.plotly_chart(fig1, use_container_width=True)
 
649
 
650
- # =============== COL 2: Count Alarm (Amber & Red Only) ===============
651
- with col2:
652
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Count (Amber & Red Only) per Hour by Tyre Position</h5>', unsafe_allow_html=True)
653
 
654
- # Filter hanya alarm Amber dan Red
655
- alarm_data = dff[dff['is_alarm'] == 1].copy()
656
- alarm_data = alarm_data[alarm_data['Alarm Status'].str.contains('Amber|Red', case=False, na=False)]
657
 
658
  if alarm_data.empty:
659
- st.warning("No Amber or Red alarm data to display.")
660
  else:
661
- # Hitung jumlah alarm per jam dan per posisi
662
- hourly_alarm_counts = alarm_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
663
-
664
- # Pastikan semua posisi (1,2,3,4) ada
665
- for pos in [1, 2, 3, 4]:
666
- if pos not in hourly_alarm_counts.columns:
667
- hourly_alarm_counts[pos] = 0
668
-
669
- hourly_alarm_counts = hourly_alarm_counts[[1, 2, 3, 4]].copy()
670
-
671
- # Melt data untuk plotting
672
- hourly_alarm_melted = hourly_alarm_counts.reset_index().melt(
673
- id_vars='hour',
674
- value_vars=[1, 2, 3, 4],
675
- var_name='Position',
676
- value_name='Alarm Count'
677
  )
678
- hourly_alarm_melted['Position'] = hourly_alarm_melted['Position'].astype(str)
679
-
680
- fig2 = px.line(
681
- hourly_alarm_melted,
682
- x='hour',
683
- y='Alarm Count',
684
- color='Position',
685
- color_discrete_map=color_map,
686
- title="Alarm Count (Amber & Red Only) per Hour by Tyre Position",
687
- labels={'Alarm Count': 'Number of Alarms', 'Position': 'Tyre Position'},
688
- line_shape='linear',
689
- template="plotly_white"
690
  )
691
 
692
- fig2.update_layout(
693
- xaxis=dict(
694
- title="Hour of Day",
695
- tickmode='array',
696
- tickvals=list(range(0, 24)),
697
- ticktext=[f"{h:02d}:00" for h in range(24)],
698
- tickangle=45
699
- ),
700
- yaxis=dict(title="Number of Alarms"),
701
- legend_title_text='Tyre Position',
702
- margin=dict(t=40, b=40, l=40, r=20),
703
- title_x=0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  )
 
 
 
 
 
705
 
706
- st.plotly_chart(fig2, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
708
- # =============== INSIGHT ===============
709
- # Insight tetap bisa menampilkan perbandingan
710
- capture_by_pos = dff['Position'].value_counts().reindex([1,2,3,4], fill_value=0)
711
- alarm_by_pos = alarm_data['Position'].value_counts().reindex([1,2,3,4], fill_value=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
- insight_text = f"""
714
- • Total data capture: Pos 1={capture_by_pos[1]}, 2={capture_by_pos[2]}, 3={capture_by_pos[3]}, 4={capture_by_pos[4]}
715
- • Total alarms (Amber/Red): Pos 1={alarm_by_pos[1]}, 2={alarm_by_pos[2]}, 3={alarm_by_pos[3]}, 4={alarm_by_pos[4]}
716
- Alarm density varies significantly between positions — suggesting different operational stress levels.
717
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
 
 
719
  st.markdown(f"""
720
  <div class="insight-box">
721
- <div class="content">
722
- {insight_text.strip()}
723
- </div>
724
  </div>
725
  """, unsafe_allow_html=True)
726
  # ================= OBJECTIVE 3 =================
 
563
  # </div>
564
  # </div>
565
  # """, unsafe_allow_html=True)
566
+ # import pandas as pd
567
+ # import plotly.express as px
568
+ # import streamlit as st
569
+
570
+ # # Judul Objective 2 — Center-aligned, elegant box-style (kanan atas kotak)
571
+ # st.markdown('''
572
+ # <div style="text-align: center; margin-bottom: 16px;">
573
+ # <div style="
574
+ # background-color: #2C2C2C;
575
+ # color: white;
576
+ # padding: 10px 16px;
577
+ # display: inline-block;
578
+ # border-radius: 6px;
579
+ # font-weight: 600;
580
+ # position: relative;
581
+ # top: 0; right: 0;
582
+ # ">
583
+ # OBJECTIVE 2: Hourly Data Capture vs Alarm Count Analysis
584
+ # </div>
585
+ # </div>
586
+ # ''', unsafe_allow_html=True)
587
 
588
+ # col1, col2 = st.columns(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
 
590
+ # # =============== COL 1: Capture Data per Jam per Tyre ===============
591
+ # with col1:
592
+ # st.markdown('<h5 style="text-align:center; margin-top: 0;">Data Capture per Hour by Tyre Position</h5>', unsafe_allow_html=True)
593
 
594
+ # # Hitung jumlah data capture per jam dan per posisi (semua data, bukan hanya alarm)
595
+ # capture_data = dff.copy()
596
+ # hourly_capture_counts = capture_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
597
 
598
+ # # Pastikan semua posisi (1,2,3,4) ada
599
+ # for pos in [1, 2, 3, 4]:
600
+ # if pos not in hourly_capture_counts.columns:
601
+ # hourly_capture_counts[pos] = 0
602
 
603
+ # hourly_capture_counts = hourly_capture_counts[[1, 2, 3, 4]].copy()
 
 
 
604
 
605
+ # # Melt data untuk plotting
606
+ # hourly_capture_melted = hourly_capture_counts.reset_index().melt(
607
+ # id_vars='hour',
608
+ # value_vars=[1, 2, 3, 4],
609
+ # var_name='Position',
610
+ # value_name='Capture Count'
611
+ # )
612
+ # hourly_capture_melted['Position'] = hourly_capture_melted['Position'].astype(str)
613
+
614
+ # # Warna sesuai preferensi
615
+ # color_map = {
616
+ # '1': '#003DA5', # Dark blue
617
+ # '2': '#7FA6E8', # Light blue
618
+ # '3': '#FFB300', # Amber (PLN yellow)
619
+ # '4': '#FFE082' # Light amber
620
+ # }
621
+
622
+ # fig1 = px.line(
623
+ # hourly_capture_melted,
624
+ # x='hour',
625
+ # y='Capture Count',
626
+ # color='Position',
627
+ # color_discrete_map=color_map,
628
+ # title="Data Capture per Hour by Tyre Position",
629
+ # labels={'Capture Count': 'Number of Records', 'Position': 'Tyre Position'},
630
+ # line_shape='linear',
631
+ # template="plotly_white"
632
+ # )
633
 
634
+ # fig1.update_layout(
635
+ # xaxis=dict(
636
+ # title="Hour of Day",
637
+ # tickmode='array',
638
+ # tickvals=list(range(0, 24)),
639
+ # ticktext=[f"{h:02d}:00" for h in range(24)],
640
+ # tickangle=45
641
+ # ),
642
+ # yaxis=dict(title="Number of Records"),
643
+ # legend_title_text='Tyre Position',
644
+ # margin=dict(t=40, b=40, l=40, r=20),
645
+ # title_x=0.5
646
+ # )
 
 
 
647
 
648
+ # st.plotly_chart(fig1, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
649
 
650
+ # # =============== COL 2: Count Alarm (Amber & Red Only) ===============
651
+ # with col2:
652
+ # st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Count (Amber & Red Only) per Hour by Tyre Position</h5>', unsafe_allow_html=True)
653
+
654
+ # # Filter hanya alarm Amber dan Red
655
+ # alarm_data = dff[dff['is_alarm'] == 1].copy()
656
+ # alarm_data = alarm_data[alarm_data['Alarm Status'].str.contains('Amber|Red', case=False, na=False)]
657
+
658
+ # if alarm_data.empty:
659
+ # st.warning("No Amber or Red alarm data to display.")
660
+ # else:
661
+ # # Hitung jumlah alarm per jam dan per posisi
662
+ # hourly_alarm_counts = alarm_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
663
+
664
+ # # Pastikan semua posisi (1,2,3,4) ada
665
+ # for pos in [1, 2, 3, 4]:
666
+ # if pos not in hourly_alarm_counts.columns:
667
+ # hourly_alarm_counts[pos] = 0
668
+
669
+ # hourly_alarm_counts = hourly_alarm_counts[[1, 2, 3, 4]].copy()
670
+
671
+ # # Melt data untuk plotting
672
+ # hourly_alarm_melted = hourly_alarm_counts.reset_index().melt(
673
+ # id_vars='hour',
674
+ # value_vars=[1, 2, 3, 4],
675
+ # var_name='Position',
676
+ # value_name='Alarm Count'
677
+ # )
678
+ # hourly_alarm_melted['Position'] = hourly_alarm_melted['Position'].astype(str)
679
+
680
+ # fig2 = px.line(
681
+ # hourly_alarm_melted,
682
+ # x='hour',
683
+ # y='Alarm Count',
684
+ # color='Position',
685
+ # color_discrete_map=color_map,
686
+ # title="Alarm Count (Amber & Red Only) per Hour by Tyre Position",
687
+ # labels={'Alarm Count': 'Number of Alarms', 'Position': 'Tyre Position'},
688
+ # line_shape='linear',
689
+ # template="plotly_white"
690
+ # )
691
+
692
+ # fig2.update_layout(
693
+ # xaxis=dict(
694
+ # title="Hour of Day",
695
+ # tickmode='array',
696
+ # tickvals=list(range(0, 24)),
697
+ # ticktext=[f"{h:02d}:00" for h in range(24)],
698
+ # tickangle=45
699
+ # ),
700
+ # yaxis=dict(title="Number of Alarms"),
701
+ # legend_title_text='Tyre Position',
702
+ # margin=dict(t=40, b=40, l=40, r=20),
703
+ # title_x=0.5
704
+ # )
705
+
706
+ # st.plotly_chart(fig2, use_container_width=True)
707
+
708
+ # # =============== INSIGHT ===============
709
+ # # Insight tetap bisa menampilkan perbandingan
710
+ # capture_by_pos = dff['Position'].value_counts().reindex([1,2,3,4], fill_value=0)
711
+ # alarm_by_pos = alarm_data['Position'].value_counts().reindex([1,2,3,4], fill_value=0)
712
+
713
+ # insight_text = f"""
714
+ # • Total data capture: Pos 1={capture_by_pos[1]}, 2={capture_by_pos[2]}, 3={capture_by_pos[3]}, 4={capture_by_pos[4]}
715
+ # • Total alarms (Amber/Red): Pos 1={alarm_by_pos[1]}, 2={alarm_by_pos[2]}, 3={alarm_by_pos[3]}, 4={alarm_by_pos[4]}
716
+ # • Alarm density varies significantly between positions — suggesting different operational stress levels.
717
+ # """
718
+
719
+ # st.markdown(f"""
720
+ # <div class="insight-box">
721
+ # <div class="content">
722
+ # {insight_text.strip()}
723
+ # </div>
724
+ # </div>
725
+ # """, unsafe_allow_html=True)
726
+ st.markdown("""
727
+ <h3 class="objective-title">OBJECTIVE 3: Alarm Frequency Analysis — When, Where, and Which Tyres Matter Most?</h3>
728
+ <small>*Showing only Red High Pressure Alarms</small>
729
+ """, unsafe_allow_html=True)
730
 
731
+ # Filter hanya data alarm
732
+ alarm_data = dff[dff['is_alarm'] == 1].copy()
733
 
734
+ col_b, col_a = st.columns(2)
 
 
735
 
736
+ # =============== COL B: Donut Charts (Distribusi Alarm per Position) ===============
737
+ with col_b:
738
+ st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Distribution by Position</h5>', unsafe_allow_html=True)
739
 
740
  if alarm_data.empty:
741
+ st.warning("No alarm data to display.")
742
  else:
743
+ # Group alarm status
744
+ alarm_data['Alarm_Category'] = alarm_data['Alarm Status'].apply(
745
+ lambda x: 'No Alarm' if 'No Alarm' in x
746
+ else 'Amber Alarm' if 'Amber' in x
747
+ else 'Red Alarm'
 
 
 
 
 
 
 
 
 
 
 
748
  )
749
+
750
+ # Buat 4 donut chart
751
+ fig_donut = make_subplots(
752
+ rows=2, cols=2,
753
+ specs=[[{'type':'domain'}, {'type':'domain'}],
754
+ [{'type':'domain'}, {'type':'domain'}]],
755
+ subplot_titles=['Position 1', 'Position 2', 'Position 3', 'Position 4']
 
 
 
 
 
756
  )
757
 
758
+ for i, pos in enumerate([1, 2, 3, 4], 1):
759
+ pos_data = alarm_data[alarm_data['Position'] == pos]
760
+ if not pos_data.empty:
761
+ counts = pos_data['Alarm_Category'].value_counts()
762
+ labels = counts.index.tolist()
763
+ values = counts.values.tolist()
764
+
765
+ # Warna: Hijau (No), Kuning (Amber), Merah (Red)
766
+ colors = ['#2E7D32', '#FFC107', '#D32F2F']
767
+
768
+ fig_donut.add_trace(
769
+ go.Pie(
770
+ labels=labels,
771
+ values=values,
772
+ name=f'Position {pos}',
773
+ hole=0.4,
774
+ marker_colors=colors
775
+ ),
776
+ row=(i - 1) // 2 + 1,
777
+ col=(i - 1) % 2 + 1
778
+ )
779
+
780
+ fig_donut.update_layout(
781
+ height=500,
782
+ showlegend=True,
783
+ margin=dict(t=40, b=20, l=20, r=20),
784
+ legend=dict(
785
+ orientation="h",
786
+ yanchor="bottom",
787
+ y=1.02,
788
+ xanchor="right",
789
+ x=1
790
+ )
791
  )
792
+ st.plotly_chart(fig_donut, use_container_width=True)
793
+
794
+ # =============== COL A: Radial Charts (Count Alarm per Jam) ===============
795
+ with col_a:
796
+ st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Count by Hour (Radial)</h5>', unsafe_allow_html=True)
797
 
798
+ if alarm_data.empty:
799
+ st.warning("No alarm data to display.")
800
+ else:
801
+ # Ambil pressure max untuk warna gradasi
802
+ max_pressure = alarm_data['Pressure (psi)'].max()
803
+ min_pressure = alarm_data['Pressure (psi)'].min()
804
+
805
+ # Buat 4 radial chart
806
+ fig_radial = make_subplots(
807
+ rows=2, cols=2,
808
+ specs=[[{'type': 'polar'}, {'type': 'polar'}],
809
+ [{'type': 'polar'}, {'type': 'polar'}]],
810
+ subplot_titles=['Front Position 1 (06:00–18:00)', 'Front Position 2 (18:00–06:00)',
811
+ 'Rear Position 3 (06:00–18:00)', 'Rear Position 4 (18:00–06:00)']
812
+ )
813
 
814
+ for i, pos in enumerate([1, 2, 3, 4], 1):
815
+ pos_data = alarm_data[alarm_data['Position'] == pos].copy()
816
+ if not pos_data.empty:
817
+ # Kelompokkan jam berdasarkan periode
818
+ if i in [1, 3]: # Pagi–sore
819
+ pos_data = pos_data[pos_data['hour'].between(6, 17, inclusive='both')]
820
+ else: # Sore–pagi
821
+ pos_data = pos_data[~pos_data['hour'].between(6, 17, inclusive='both')]
822
+
823
+ if not pos_data.empty:
824
+ hourly_counts = pos_data.groupby('hour').size().reindex(range(24), fill_value=0)
825
+ if i in [1, 3]: # 06:00–18:00
826
+ hourly_counts = hourly_counts[6:18]
827
+ else: # 18:00���06:00
828
+ hourly_counts = pd.concat([hourly_counts[18:], hourly_counts[:6]]).reindex(range(12), fill_value=0)
829
+
830
+ # Warna berdasarkan rata-rata pressure
831
+ avg_pressure_per_hour = pos_data.groupby('hour')['Pressure (psi)'].mean().reindex(hourly_counts.index, fill_value=0)
832
+ colorscale = avg_pressure_per_hour
833
+
834
+ fig_radial.add_trace(
835
+ go.Barpolar(
836
+ r=hourly_counts.values,
837
+ theta=hourly_counts.index * 30, # 12 jam * 30° = 360°
838
+ name=f'Position {pos}',
839
+ marker=dict(
840
+ color=colorscale,
841
+ colorscale='Reds',
842
+ cmin=min_pressure,
843
+ cmax=max_pressure
844
+ ),
845
+ opacity=0.8
846
+ ),
847
+ row=(i - 1) // 2 + 1,
848
+ col=(i - 1) % 2 + 1
849
+ )
850
 
851
+ fig_radial.update_layout(
852
+ height=600,
853
+ showlegend=False,
854
+ margin=dict(t=60, b=20, l=20, r=20)
855
+ )
856
+ st.plotly_chart(fig_radial, use_container_width=True)
857
+
858
+ # =============== INSIGHT 3 ===============
859
+ if alarm_data.empty:
860
+ insight_text = "• No alarm data available for analysis."
861
+ else:
862
+ # Insight tetap sama
863
+ alarm_hours = alarm_data['hour']
864
+
865
+ def hour_to_band(h):
866
+ if 0 <= h < 6: return "00:00–06:00 (Night)"
867
+ if 6 <= h < 12: return "06:00–12:00 (Morning)"
868
+ if 12 <= h < 18: return "12:00–18:00 (Afternoon)"
869
+ return "18:00–00:00 (Evening)"
870
+
871
+ alarm_hours_df = pd.DataFrame({'hour': alarm_hours})
872
+ alarm_hours_df['band'] = alarm_hours_df['hour'].apply(hour_to_band)
873
+ band_counts = alarm_hours_df['band'].value_counts().sort_index()
874
+
875
+ top_bands = band_counts.nlargest(2)
876
+ dominant_band = top_bands.index[0] if len(top_bands) > 0 else "N/A"
877
+ second_dominant_band = top_bands.index[1] if len(top_bands) > 1 else "N/A"
878
+
879
+ dominant_pct = (top_bands.iloc[0] / band_counts.sum() * 100) if len(top_bands) > 0 else 0
880
+ second_pct = (top_bands.iloc[1] / band_counts.sum() * 100) if len(top_bands) > 1 else 0
881
+
882
+ front_alarms = alarm_data[alarm_data['Position'].isin([1, 2])].shape[0]
883
+ rear_alarms = alarm_data[alarm_data['Position'].isin([3, 4])].shape[0]
884
+ total_alarms = front_alarms + rear_alarms
885
+ front_pct = front_alarms / total_alarms * 100 if total_alarms > 0 else 0
886
+
887
+ top_zone = alarm_data['Zone'].value_counts().index[0] if not alarm_data.empty else "N/A"
888
+
889
+ insight_lines = [
890
+ f"• {dominant_band} is the dominant alarm period ({dominant_pct:.1f}% of all alarms).",
891
+ f"• {second_dominant_band} is the second-highest period ({second_pct:.1f}% of alarms)."
892
+ ]
893
+ if front_alarms > 0:
894
+ insight_lines.append(f"• Front tyres (Pos 1 & 2) account for {front_pct:.1f}% of all alarms, indicating higher stress or usage intensity upfront.")
895
+ if top_zone != "N/A":
896
+ insight_lines.append(f"• Zone {top_zone} records the highest alarm frequency across all positions.")
897
+ insight_lines.append("• Alarm clustering in specific hours and front positions suggests opportunity for targeted inspection scheduling.")
898
+
899
+ insight_text = "\n".join(insight_lines)
900
 
901
+ # =============== DISPLAY INSIGHT ===============
902
  st.markdown(f"""
903
  <div class="insight-box">
904
+ <div class="content">
905
+ {insight_text}
906
+ </div>
907
  </div>
908
  """, unsafe_allow_html=True)
909
  # ================= OBJECTIVE 3 =================