import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import panel as pn
import re
from openpyxl import load_workbook
from io import BytesIO
from datetime import datetime
import param
import hvplot.pandas
from io import StringIO
import html
import holoviews as hv
from holoviews import opts
from bokeh.plotting import figure, show
from bokeh.models import TextInput, HoverTool, WheelZoomTool, LinearAxis, Range1d, ColumnDataSource, NumeralTickFormatter, LabelSet, Legend, LegendItem, CategoricalColorMapper, FactorRange, Title, DatetimeTickFormatter, CustomJS, CustomJSHover, CDSView, BooleanFilter, HTMLTemplateFormatter, ImageURL, Div, CustomJSTickFormatter
from bokeh.palettes import Category10, Category20
from bokeh.layouts import column, row
import warnings
import bokeh.plotting as bkp
import base64
import io
import os
from PIL import Image
from bokeh.transform import factor_cmap, dodge # 03/27
from bokeh.models.glyphs import VBar
from bokeh.palettes import Category20c
#from bokeh.transform import cumsum
import numpy as np
import calendar # 03/04
from bokeh.palettes import Category20c
import numpy as np
from bokeh.models import Label, Span #04/01
from bokeh.models import LinearColorMapper, ColorBar, BasicTicker #04/07
from bokeh.palettes import Viridis256 #04/07
from bokeh.transform import transform #04/07
#from bokeh.models import TableColumn, NumberFormatter, DataTable #04/08
#------ 04/08 --------------------
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.styles import Alignment, PatternFill, Font, Border, Side
from openpyxl.utils import get_column_letter
###################################################################################################################################
# Enable the Panel extension
pn.extension()
# Suppress all warnings
warnings.filterwarnings("ignore")
# Load the Tabulator extension
pn.extension('tabulator')
# Initialize HoloViews and Panel extensions
hv.extension('bokeh')
##############################################################################################################################
# Define date and path
##############################################################################################################################
# Define paths and file names
input_file_formatted = 'CM-Transfer_Project-Overview.xlsx'
##############################################################################################################################
# Load workbook
##############################################################################################################################
# Load the Excel files into pandas DataFrames
try:
df_Summary = pd.read_excel(input_file_formatted, sheet_name='Summary', index_col=False)
df_Priority = pd.read_excel(input_file_formatted, sheet_name='CM-Priority', index_col=False)
df_Snapshot = pd.read_excel(input_file_formatted, sheet_name='Snapshot', index_col=False)
df_TurnoverReport = pd.read_excel(input_file_formatted, sheet_name='CM-TurnoverReport', index_col=False)
df_Backlog = pd.read_excel(input_file_formatted, sheet_name='CM-Backlog', index_col=False)
df_WIP = pd.read_excel(input_file_formatted, sheet_name='CM-WIP', index_col=False)
df_PendingReport = pd.read_excel(input_file_formatted, sheet_name='PendingReport', index_col=False)
df_ADCNReport = pd.read_excel(input_file_formatted, sheet_name='CM-ADCNReport', index_col=False)
df_Historic = pd.read_excel(input_file_formatted, sheet_name='CM-Historic', index_col=False)
df_Timeline = pd.read_excel(input_file_formatted, sheet_name='Timeline', index_col=False) # 05/19
print("Input files loaded successfully.")
except FileNotFoundError as e:
print(f"File not found: {e}")
exit()
# Load the workbook
try:
workbook = load_workbook(input_file_formatted)
print(f"Successfully loaded '{input_file_formatted}'")
except FileNotFoundError as e:
print(f"File not found: {e}")
exit()
#--------------------------------------------------
# Define 'file date' based on the value od W2 in df_Backlog
#--------------------------------------------------
# Open the workbook
workbook = load_workbook(input_file_formatted, data_only=True) # 'data_only' ensures formulas are evaluated
backlog_sheet = workbook["CM-Backlog"] # Load the CM-Backlog sheet
# Extract file date from cell W2
file_date = backlog_sheet["W2"].value
if file_date:
print("File Date:", file_date)
else:
print("Could not retrieve File Date from W2.")
# Close the workbook
workbook.close()
################################################################
# Filter out "Canceled" or "To be transferred" from df_priority
################################################################
# Filter out rows where 'Production Status' is 'Canceled' or 'To be transferred' or 'Officially transferred'
if 'Production Status' in df_Priority.columns:
df_Priority = df_Priority[~df_Priority['Production Status'].isin(['Canceled', 'To be transferred', 'Officially transferred'])]
#-------------------- 03/11 - Convert 'Priority' column to int ------------------
# Ensure 'Priority' is numeric, converting errors to NaN
df_Priority['Priority'] = pd.to_numeric(df_Priority['Priority'], errors='coerce')
# Remove NaN values
df_Priority.dropna(subset=['Priority'], inplace=True) #Remove rows with NaN in 'Priority'
# Convert to integer
df_Priority['Priority'] = df_Priority['Priority'].astype(int)
#-----------------------------------------------------------------------------------
#----------------------------------------------------------
# 02/11 - Change 'Phase 4' or 'Phase 5' with 'Phase 4-5'
#----------------------------------------------------------
# For df_Priority
if 'Program' in df_Priority.columns and 'Pty Indice' in df_Priority.columns:
mask = (
df_Priority['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Priority['Pty Indice'].str.contains('Phase5', na=False)
)
df_Priority.loc[mask, 'Program'] = 'Phase 4-5'
# For df_Backlog
if 'Program' in df_Backlog.columns and 'Pty Indice' in df_Backlog.columns:
mask = (
df_Backlog['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Backlog['Pty Indice'].str.contains('Phase5', na=False)
)
df_Backlog.loc[mask, 'Program'] = 'Phase 4-5'
# For df_TurnoverReport
# Check if the DataFrame is empty
if df_TurnoverReport.empty:
print("The DataFrame is empty. Skipping operations.")
else:
# Ensure 'Pty Indice' is a string column and handle missing values
df_TurnoverReport['Pty Indice'] = df_TurnoverReport['Pty Indice'].astype(str).fillna('')
# Apply the condition to update the 'Program' column
if 'Program' in df_TurnoverReport.columns and 'Pty Indice' in df_TurnoverReport.columns:
mask = (
df_TurnoverReport['Program'].isin(['Phase 4', 'Phase 5']) &
~df_TurnoverReport['Pty Indice'].str.contains('Phase5', case=False, na=False)
)
df_TurnoverReport.loc[mask, 'Program'] = 'Phase 4-5'
else:
print("Required columns ('Program' or 'Pty Indice') are missing in the DataFrame.")
# For df_Historic
if 'Program' in df_Historic.columns and 'Pty Indice' in df_Historic.columns:
mask = (
df_Historic['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Historic['Pty Indice'].str.contains('Phase5', na=False)
)
df_Historic.loc[mask, 'Program'] = 'Phase 4-5'
# For df_Snapshot (new addition)
if 'Program' in df_Snapshot.columns and 'Pty Indice' in df_Snapshot.columns:
mask = (
df_Snapshot['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Snapshot['Pty Indice'].str.contains('Phase5', na=False)
)
df_Snapshot.loc[mask, 'Program'] = 'Phase 4-5'
#----------------------------------------------------------
#------------------------------------------------------------------------------------------
# 03/19 - Only keep 'Program' = 'Phase 4-5', 'EMBRAER', SIKORSKY', '1stTB', '2ndTB'
#------------------------------------------------------------------------------------------
# Define the allowed values for the 'Program' column
allowed_programs = ['Phase 4-5', 'EMBRAER', 'SIKORSKY', 'COMAC', '1stTB', '2ndTB']
# Apply filtering to each DataFrame
df_Priority = df_Priority[df_Priority['Program'].isin(allowed_programs)]
df_Backlog = df_Backlog[df_Backlog['Program'].isin(allowed_programs)]
df_TurnoverReport = df_TurnoverReport[df_TurnoverReport['Program'].isin(allowed_programs)]
df_Historic = df_Historic[df_Historic['Program'].isin(allowed_programs)]
df_Snapshot = df_Snapshot[df_Snapshot['Program'].isin(allowed_programs)]
#------------------------------------------------------------------------------------------
#*****************************************************************************************************************************
# |General Overview| - Table creation
#*****************************************************************************************************************************
#--------------------------------------------------------------------------
# Create pivot_table_combined
#--------------------------------------------------------------------------
# Create a new column 'Industrialization'
df_Snapshot['Industrialization'] = df_Snapshot['Production Status'].apply(
lambda x: 'Industrialized' if x.strip() in ['Industrialized', 'Completed'] else 'Not Industrialized'
)
# Fill empty 'Qty WIP' with 0
df_Snapshot['Qty WIP'] = df_Snapshot['Qty WIP'].fillna(0)
#######################################################
# Assuming df_Priority has the columns 'Pty Indice' and 'Priority'
priority_mapping = df_Priority.set_index('Pty Indice')['Priority'].to_dict()
# Create the pivot table without 'Priority'
pivot_table_13 = pd.pivot_table(df_Snapshot,
index=['Top-Level Status', 'Industrialization', 'Product Category', 'Pty Indice'],
values=['IDD Backlog Qty', 'Remain. crit. Qty', 'Qty clear to build', 'Qty WIP', 'Shipped', 'Critical Qty'],
aggfunc='sum',
fill_value=0).reset_index()
# Add the 'Priority' column to pivot_table_13 using the mapping
pivot_table_13['Priority'] = pivot_table_13['Pty Indice'].map(priority_mapping)
###################### New 08/12 #########################################
# Add the 'Program' column to pivot_table_13 using the mapping
program_mapping = df_Priority.set_index('Pty Indice')['Program'].to_dict()
pivot_table_13['Program'] = pivot_table_13['Pty Indice'].map(program_mapping)
#########################################################
# Merge df_Backlog with df_Snapshot to get additional columns
merged_df = pd.merge(df_Backlog, df_Snapshot[['Pty Indice', 'Qty clear to build', 'Top-Level Status', 'Qty WIP', 'Industrialization', 'Product Category', 'Shipped', 'Critical Qty', 'Priority']], on='Pty Indice', how='left')
# Rename 'Backlog row Qty' to 'IDD Backlog Qty'
merged_df.rename(columns={'Backlog row Qty': 'IDD Backlog Qty'}, inplace=True)
# Create 'Order Type' column
merged_df['Order Type'] = merged_df['Order'].apply(lambda x: 'DX/DO' if str(x).startswith('D') else ('Standard' if 'NC' not in str(x) else None))
# Aggregate IDD Backlog Qty by 'Pty Indice', 'Order Type', and other relevant columns
unique_merged_df = merged_df.groupby(
['Pty Indice', 'Top-Level Status', 'Industrialization', 'Product Category', 'Order Type']
).agg({
'IDD Backlog Qty': 'sum', # Sum IDD Backlog Qty for unique combinations
'Qty clear to build': 'sum',
'Remain. crit. Qty': 'sum',
'Qty WIP': 'sum',
}).reset_index()
# Create a pivot table to separate IDD Backlog Qty by Order Type
pivot_order_type = unique_merged_df.pivot_table(
index=['Pty Indice', 'Top-Level Status', 'Industrialization', 'Product Category'],
columns='Order Type',
values='IDD Backlog Qty',
aggfunc='sum', # Sum values to ensure accurate totals
fill_value=0 # Fill NaNs with 0
).reset_index()
# Flatten MultiIndex columns
pivot_order_type.columns = [f'{col[0]}_{col[1]}' if col[1] != '' else col[0] for col in pivot_order_type.columns]
# Rename columns to be more descriptive
pivot_order_type.columns = ['Pty Indice', 'Top-Level Status', 'Industrialization', 'Product Category', 'DX_Order_Type', 'Standard_Order_Type']
# Merge the two pivot tables
pivot_table_combined = pd.merge(
pivot_table_13,
pivot_order_type,
on=['Pty Indice', 'Top-Level Status', 'Industrialization', 'Product Category'],
how='left'
)
# Rename the remaining columns for clarity
pivot_table_combined.rename(columns={
'Top-Level Status_x': 'Top-Level Status',
'Industrialization_x': 'Industrialization',
'Product Category_x': 'Product Category',
'DX_Order_Type': 'DPAS Order',
'Standard_Order_Type':'Standard Order',
'Shipped':'Qty Shipped',
'Critical Qty': 'Total Critical Qty'
}, inplace=True)
#Ordering pivot_table_combined to get the column in the relevant order of relevance for the bar chart
# Define the desired column order
desired_column_order = [
'Industrialization',
'Top-Level Status',
'Product Category',
'Priority',
'Pty Indice',
'Standard Order',
'DPAS Order',
'Qty WIP',
'Qty clear to build',
'Total Critical Qty',
'Qty Shipped',
'Remain. crit. Qty',
'IDD Backlog Qty'
]
# Reorder the columns in pivot_table_combined
pivot_table_combined = pivot_table_combined[desired_column_order]
# Ensure 'Priority' is in the correct data type (int or float)
# Convert 'Priority' column to numeric, coercing errors (non-numeric entries will become NaN)
pivot_table_combined['Priority'] = pd.to_numeric(pivot_table_combined['Priority'], errors='coerce')
pivot_table_combined['Priority'].fillna(999, inplace=True)
# Fill all other than column then 'Priotity' containing NaN values with 0 in the entire DataFrame
pivot_table_combined.fillna(0, inplace=True)
# Define the custom sort orders, including the additional categories
industrialization_order = pivot_table_combined['Industrialization'].unique().tolist()
top_level_status_order = pivot_table_combined['Top-Level Status'].unique().tolist() + ['All Top-Level Status']
product_category_order = pivot_table_combined['Product Category'].unique().tolist() + ['All Product Categories']
# Set the categories and order for sorting
pivot_table_combined['Industrialization'] = pd.Categorical(
pivot_table_combined['Industrialization'],
categories=pivot_table_combined['Industrialization'].unique().tolist(),
ordered=True
)
pivot_table_combined['Top-Level Status'] = pd.Categorical(
pivot_table_combined['Top-Level Status'],
categories=pivot_table_combined['Top-Level Status'].unique().tolist(),
ordered=True
)
pivot_table_combined['Product Category'] = pd.Categorical(
pivot_table_combined['Product Category'],
categories=pivot_table_combined['Product Category'].unique().tolist(),
ordered=True
)
# Sort by Priority; specify na_position if needed (e.g., na_position='last')
pivot_table_combined.sort_values(by=['Priority', 'Pty Indice'], inplace=True, na_position='last')
#--------------------------------------------------------------------------
# Create pivot_table_14
#--------------------------------------------------------------------------
##############################################################################################################################
# Financial KPI datafram to be update with df_Historic
# --> To be update 09/23 to use df_Historic instead of df_Snapshot to calculate the 'Realized sales' and 'Realized Margin'. The calculation should be based on the real data from the df_Historic trunover Report including the change of price over time
### Need to create 'IDD Current Sales (Total)' and ['IDD Current Margin (Total)'] should be based on df_Historic --> AVG of the sales and margin should work
# --> # Need to use 'IDD AVG realized sales price [USD]' & ['IDD AVG realized Margin[%]'] instead of the 'IDD Current Sales (Total)' & ['IDD Current Margin (Total)']
# New columns introduced in df_Snapshot:
# df_snapshot['IDD AVG realized sales price [USD]']
# df_snapshot['IDD AVG realized Margin Standard [USD]']
# df_snapshot['IDD AVG realized Margin [%]']
##############################################################################################################################
# Creating Graph#14 [IDD Expected Total Sales & IDD Marge per Pty Indice by Top-Level Status, Production Status & Product Category]
##############################################################################################################################
# Calculate 'IDD Expected Total Sales'
df_Snapshot['IDD Expected Total Sales'] = df_Snapshot['IDD Backlog Qty'] * df_Snapshot['IDD Sale Price']
# Calculate 'IDD Current Total Sales'
df_Snapshot['IDD Current Sales (Total)'] = df_Snapshot['Shipped'] * df_Snapshot['IDD Sale Price']
# Calculate 'IDD Expected Total Marge'
df_Snapshot['IDD Expected Total Margin'] = df_Snapshot['IDD Backlog Qty'] * df_Snapshot['IDD Marge Standard (unit)']
# Calculate 'IDD Current Total Marge'
df_Snapshot['IDD Current Margin (Total)'] = df_Snapshot['Shipped'] * df_Snapshot['IDD Marge Standard (unit)']
df_Snapshot['Industrialization'] = df_Snapshot['Production Status'].apply(
lambda x: 'Industrialized' if x.strip() in ['Industrialized', 'Completed'] else 'Not Industrialized'
)
##############################################################################################################################
# Assuming df_Priority has the columns 'Pty Indice' and 'Priority'
priority_mapping = df_Priority.set_index('Pty Indice')['Priority'].to_dict()
# Create the pivot table without 'Priority'
pivot_table_14 = pd.pivot_table(df_Snapshot,
index=['Top-Level Status', 'Industrialization', 'Product Category', 'Pty Indice'],
values=['IDD Expected Total Sales', 'IDD Expected Total Margin', 'IDD Current Margin (%)', 'Priority', 'Critical Qty', 'Shipped', 'IDD Backlog Qty', 'IDD Current Sales (Total)', 'IDD Current Margin (Total)', 'IDD Expected ROI (Total)', 'IDD AVG realized sales price [USD]', 'IDD AVG realized Margin Standard [USD]', 'IDD AVG realized Margin [%]'],
aggfunc='sum',
fill_value=0).reset_index()
# Add the 'Priority' column to pivot_table_13 using the mapping
pivot_table_14['Priority'] = pivot_table_14['Pty Indice'].map(priority_mapping)
# Add the 'Program' column to pivot_table_14 using the mapping
pivot_table_14['Program'] = pivot_table_14['Pty Indice'].map(program_mapping)
# Map 'DPAS Order' from pivot_table_combined on 'Pty Indice' column
pivot_table_14 = pivot_table_14.merge(pivot_table_combined[['Pty Indice', 'DPAS Order']], on='Pty Indice', how='left')
# Calculate '% Completion' and round to one decimal place
pivot_table_14['% Completion'] = round((pivot_table_14['Shipped'] / pivot_table_14['Critical Qty']) * 100, 1)
pivot_table_14['% Completion Total Backlog'] = round((pivot_table_14['Shipped'] / (pivot_table_14['IDD Backlog Qty'] + pivot_table_14['Shipped'])) * 100, 1) # New 09/26, updated 10/08 because Total Backlog should be 'IDD Backlog Qty' + 'Shipped' to consider the initial backlog
# Calculate '% DPAS Order' and round to one decimal place
pivot_table_14['% DPAS Order'] = round((pivot_table_14['DPAS Order'] / pivot_table_14['IDD Backlog Qty']) * 100, 1)
# Define the sort order for both columns
industrialization_order = ['Industrialized', 'Not Industrialized']
top_level_status_order = ['Clear-to-Build', 'Short', 'Completed - No Backlog'] # Update 09/16
# Set the categories and order for sorting
pivot_table_14['Industrialization'] = pd.Categorical(pivot_table_14['Industrialization'], categories=industrialization_order, ordered=True)
pivot_table_14['Top-Level Status'] = pd.Categorical(pivot_table_14['Top-Level Status'], categories=top_level_status_order, ordered=True)
# Sort by Industrialization, then by Top-Level Status, and finally by Product Category
pivot_table_14.sort_values(by=['Industrialization', 'Top-Level Status', 'Product Category'], inplace=True)
# delete '%' from 'IDD Current Margin (%)'
pivot_table_14['IDD Current Margin (%)'] = pivot_table_14['IDD Current Margin (%)'].str.replace('%', '').astype(float)
# Round the values to one decimal place
pivot_table_14['IDD Current Margin (%)'] = pivot_table_14['IDD Current Margin (%)'].round()
# Convert 'IDD Expected ROI (Total)' to string, replacing NaN with '0%'
pivot_table_14['IDD Expected ROI (Total)'] = pivot_table_14['IDD Expected ROI (Total)'].fillna('0%').astype(str)
# Remove '%' and convert to float
pivot_table_14['IDD Expected ROI (Total)'] = pivot_table_14['IDD Expected ROI (Total)'].str.replace('%', '').astype(float)
# Round the values to one decimal place
pivot_table_14['IDD Expected ROI (Total)'] = pivot_table_14['IDD Expected ROI (Total)'].round()
# Round the values to one decimal place
pivot_table_14['IDD Expected Total Sales'] = pivot_table_14['IDD Expected Total Sales'].round()
pivot_table_14['IDD Expected Total Margin'] = pivot_table_14['IDD Expected Total Margin'].round()
pivot_table_14['IDD Current Sales (Total)'] = pivot_table_14['IDD Current Sales (Total)'].round()
pivot_table_14['IDD Current Margin (Total)'] = pivot_table_14['IDD Current Margin (Total)'].round()
#######################################################################
# Step 1: Clean and convert 'IDD AVG realized Margin [%]'
pivot_table_14['IDD AVG realized Margin [%]'] = pivot_table_14['IDD AVG realized Margin [%]'].str.replace('%', '').astype(float)
# Round the margin to 1 decimal place
pivot_table_14['IDD AVG realized Margin [%]'] = pivot_table_14['IDD AVG realized Margin [%]'].round(1)
# Step 2: Clean and convert 'IDD AVG realized Margin Standard [USD]'
# Remove '$' and ',' before conversion
pivot_table_14['IDD AVG realized Margin Standard [USD]'] = (
pivot_table_14['IDD AVG realized Margin Standard [USD]']
.replace({'\\$': '', ',': ''}, regex=True)
)
# Use pd.to_numeric to handle conversion and coercion
pivot_table_14['IDD AVG realized Margin Standard [USD]'] = pd.to_numeric(pivot_table_14['IDD AVG realized Margin Standard [USD]'], errors='coerce').round(2)
# Step 3: Clean and convert 'IDD AVG realized sales price [USD]'
pivot_table_14['IDD AVG realized sales price [USD]'] = (
pivot_table_14['IDD AVG realized sales price [USD]']
.replace({'\\$': '', ',': ''}, regex=True)
)
# Use pd.to_numeric to handle conversion and coercion
pivot_table_14['IDD AVG realized sales price [USD]'] = pd.to_numeric(pivot_table_14['IDD AVG realized sales price [USD]'], errors='coerce').round(2)
# Step 4: Calculate 'IDD Realized Sales'
pivot_table_14['IDD Realized Sales'] = pivot_table_14['IDD AVG realized sales price [USD]'] * pivot_table_14['Shipped']
# Step 5: Calculate 'IDD Realized Margin'
pivot_table_14['IDD Realized Margin'] = pivot_table_14['IDD AVG realized Margin Standard [USD]'] * pivot_table_14['Shipped']
# Step 6: Format 'IDD AVG realized sales price [USD]' as currency and replace NaN with 0
pivot_table_14['IDD AVG realized sales price [USD]'] = pivot_table_14['IDD AVG realized sales price [USD]'].apply(lambda x: f"${x:,.2f}" if pd.notna(x) else '$0.00')
# Step 7: Format 'IDD AVG realized Margin Standard [USD]' as currency and replace NaN with 0
pivot_table_14['IDD AVG realized Margin Standard [USD]'] = pivot_table_14['IDD AVG realized Margin Standard [USD]'].apply(lambda x: f"${x:,.2f}" if pd.notna(x) else '$0.00')
# Step 8: Format 'IDD AVG realized Margin [%]' as percentage and replace NaN with 0
pivot_table_14['IDD AVG realized Margin [%]'] = pivot_table_14['IDD AVG realized Margin [%]'].apply(lambda x: f"{x:.1f}%" if pd.notna(x) else '0.0%')
# Ensure 'Priority' is in the correct data type (int or float)
pivot_table_14['Priority'] = pd.to_numeric(pivot_table_14['Priority'], errors='coerce')
# Sort the DataFrame by 'Priority' in ascending order (use ascending=False for descending order)
#pivot_table_14 = pivot_table_14.sort_values(by='Priority', ascending=True)
pivot_table_14.sort_values(by=['Priority', 'Pty Indice'], inplace=True)
#--------------------------------------------------------------------------
# Create pivot_table_15 - Production metrics Expected Time, Actual Time, Standard Deviation
#--------------------------------------------------------------------------
# Create df_MaekArchi = |CM-MakeArchitecture|
df_MakeArchi = pd.read_excel(input_file_formatted, sheet_name='CM-MakeArchitecture', index_col=False)
# df_Production = df_Snapshot['Priority', 'Pty Indice', 'Program', 'Expected Time [hour]', 'Actual Time [hour]', 'Standard Deviation [hour]']
df_Production = df_Snapshot.copy()
#Selected relevant columns
df_Production = df_Production[['Top-Level Status', 'Priority', 'Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Production Status', 'Product Category', 'Program', 'Max Expected Time (full ASSY)[hour]', 'Avg Actual Time (full ASSY)[hour]', 'Max Standard Deviation [hour]', 'Total WO Count']]
# Rename the selected columns
df_Production = df_Production.rename(columns={
'Max Expected Time (full ASSY)[hour]': 'Expected Time',
'Avg Actual Time (full ASSY)[hour]': 'Actual Time',
'Max Standard Deviation [hour]': 'Standard Deviation',
})
df_Production['Industrialization'] = df_Production['Production Status'].apply(
lambda x: 'Industrialized' if x.strip() in ['Industrialized', 'Completed'] else 'Not Industrialized'
)
# Ensure 'Priority' is in the correct data type (int or float)
df_Production['Priority'] = pd.to_numeric(df_Production['Priority'], errors='coerce')
# Define sort orders
top_level_status_order_prod = sorted(df_Production['Top-Level Status'].unique().tolist()) # Ensure it's unique and sorted
product_category_order_prod = sorted(df_Production['Product Category'].unique().tolist()) # Ensure it's unique and sorted
# Optionally, you might want to fill or drop NaNs depending on your requirement
# For example, fill NaNs with a default value 999:
df_Production['Priority'].fillna(999, inplace=True)
# Fill all other than column then 'Priotity' containing NaN values with 0 in the entire DataFrame
df_Production.fillna(0, inplace=True)
# Convert 'Priority' column to integers
df_Production['Priority'] = df_Production['Priority'].astype(int)
# Set categorical data type for 'Industrialization' column
df_Production['Industrialization'] = pd.Categorical(
df_Production['Industrialization'],
categories=['Industrialized', 'Not Industrialized'],
ordered=False
)
# Set categorical data types with the specified sort orders for other columns
df_Production['Top-Level Status'] = pd.Categorical(
df_Production['Top-Level Status'],
categories=top_level_status_order_prod,
ordered=False
)
df_Production['Product Category'] = pd.Categorical(
df_Production['Product Category'],
categories=product_category_order_prod,
ordered=False
)
# Sort by Priority; specify na_position if needed (e.g., na_position='last')
df_Production.sort_values(by=['Priority', 'Pty Indice'], inplace=True, na_position='last')
###########################################################################################################################################
## Map 'WO_Count' from Top-Level (Level = 0) from |CM-MakeArchitecture| (df_MakeArchi) on 'Pty Indice' and rename 'Top-Level WO Count'
## Map 'Avg Actual Time (unit)[hour]' from Top-Level (Level = 0) from |CM-MakeArchitecture| (df_MakeArchi) on 'Pty Indice' and rename 'Actual Time (Top-Level only)'
###########################################################################################################################################
# Include 'Total Top-Level Qty' and 'Total Components Qty' in he table 'Top-Level WO Count'*'Qty per WO' and 'Total WO Count'*'Total Components Qty'
# Filter df_MakeArchi where Level == 0 to get Top-Level
top_level_df = df_MakeArchi[df_MakeArchi['Level'] == 0]
# Filter df_MakeArchi where Level != 0 to get sub-Level
sub_level_df = df_MakeArchi[df_MakeArchi['Level'] != 0]
# Create a dictionary mapping Pty Indice to WO_Count and Avg Actual Time (unit)[hour]
wo_count_top_level_mapping = dict(zip(top_level_df['Pty Indice'], top_level_df['WO_Count']))
avg_actual_time_mapping = dict(zip(top_level_df['Pty Indice'], top_level_df['Avg Actual Time (unit)[hour]']))
# Map 'Top-Level WO Count' to df_Production using the mapping dictionary
df_Production['Top-Level WO Count'] = df_Production['Pty Indice'].map(wo_count_top_level_mapping)
df_Production['Actual Time (Top-Level only)'] = df_Production['Pty Indice'].map(avg_actual_time_mapping)
#New 09/10 --> Include 'Total Top-Level Qty' and 'Total Components Qty' in he table 'Top-Level WO Count'*'Qty per WO' and 'Total WO Count'*'Total Components Qty'
qty_top_level_count_mapping = dict(zip(top_level_df['Pty Indice'], top_level_df['Qty_Count']))
qty_sub_level_count_mapping = dict(zip(sub_level_df['Pty Indice'], sub_level_df['Qty_Count']))
df_Production['Total Top-Level Qty'] = df_Production['Pty Indice'].map(qty_top_level_count_mapping)
df_Production['Total sub-Level Qty'] = df_Production['Pty Indice'].map(qty_sub_level_count_mapping)
# Fill NaN values in specific columns with 0
df_Production[['Total WO Count', 'Top-Level WO Count', 'Total Top-Level Qty', 'Total sub-Level Qty']] = df_Production[['Total WO Count', 'Top-Level WO Count', 'Total Top-Level Qty', 'Total sub-Level Qty']].fillna(0)
# Ensure there are no infinite values (inf) in the columns
df_Production[['Total WO Count', 'Top-Level WO Count', 'Total Top-Level Qty', 'Total sub-Level Qty']] = df_Production[['Total WO Count', 'Top-Level WO Count', 'Total Top-Level Qty', 'Total sub-Level Qty']].replace([np.inf, -np.inf], 0)
# Convert 'Total WO Count' and 'Top-Level WO Count' to integer
#------------------- 03/24 ------------------------
# 1. Replace the problematic string pattern with 0
df_Production['Total WO Count'] = df_Production['Total WO Count'].replace(
r'0 \(similar to \d+-\d+-\d+\)',
'0',
regex=True
)
# 2. Convert to numeric (non-numeric becomes NaN), then fill NaN with 0
df_Production['Total WO Count'] = pd.to_numeric(
df_Production['Total WO Count'],
errors='coerce'
).fillna(0)
# 3. Finally convert to integer
df_Production['Total WO Count'] = df_Production['Total WO Count'].astype(int)
#---------------------------------------------------
df_Production['Top-Level WO Count'] = df_Production['Top-Level WO Count'].astype(int)
df_Production['Total Top-Level Qty'] = df_Production['Total Top-Level Qty'].astype(int)
df_Production['Total sub-Level Qty'] = df_Production['Total sub-Level Qty'].astype(int)
# Optionally fill NaNs in 'Actual Time (Top-Level only)' with 0 or another value if necessary
df_Production['Actual Time (Top-Level only)'] = df_Production['Actual Time (Top-Level only)'].fillna(0)
# Round(1) 'Expected Time', 'Actual Time, 'Standard Deviation', 'Actual Time (Top-Level only)'
df_Production['Expected Time'] = df_Production['Expected Time'].round(1)
df_Production['Actual Time'] = df_Production['Actual Time'].round(1)
df_Production['Standard Deviation'] = df_Production['Standard Deviation'].round(1)
df_Production['Actual Time (Top-Level only)'] = df_Production['Actual Time (Top-Level only)'].round(1)
#Rename 'Expected Time' to 'Standard Time' for better clarifity on the graph
df_Production.rename(columns={'Expected Time': 'Standard Time (Routing, full ASSY)'}, inplace=True)
df_Production.rename(columns={'Actual Time': 'Actual Time (AVG Prod, full ASSY)'}, inplace=True)
df_Production.rename(columns={'Standard Deviation': 'Standard Deviation (on Actual Time, full ASSY)'}, inplace=True)
df_Production.rename(columns={'Actual Time (Top-Level only)': 'Actual Time (AVG Prod, Top-Level only)'}, inplace=True)
#Create Pivot_table_15 base on df_Production only for |General Overview| & Only keep Pivot_table_15['Program'] = 'Phase 4'
pivot_table_15 = df_Production.copy()
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# |4 cadrans|
##############################################################################################################################
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Create titles for each quadrant
engineering_title = "Engineering"
sales_title = "Sales"
supply_chain_title = "Supply Chain"
production_title = "Production"
# Create a mapping dictionary from df_Priority
indice_to_program = dict(zip(df_Priority['Pty Indice'], df_Priority['Program']))
indice_to_priority = dict(zip(df_Priority['Pty Indice'], df_Priority['Priority']))
# Apply mapping to create 'Program' column in df_Summary and df_Backlog
df_Summary['Program'] = df_Summary['Pty Indice'].map(indice_to_program)
df_Backlog['Program'] = df_Backlog['Pty Indice'].map(indice_to_program)
df_TurnoverReport['Program'] = df_TurnoverReport['Pty Indice'].map(indice_to_program)
df_Snapshot['Program'] = df_Snapshot['Pty Indice'].map(indice_to_program)
df_WIP['Program'] = df_WIP['Pty Indice'].map(indice_to_program)
df_PendingReport['Priority'] = df_PendingReport['Pty Indice'].map(indice_to_priority)
###################################################
# Renaming
###################################################
# Rename certain columns and assign back to df_WIP
df_WIP = df_WIP.rename(columns={
'Qty Ordered': 'WO Qty',
})
# Rename certain columns and assign back to df_Backlog
df_Backlog = df_Backlog.rename(columns={
#'Backlog row Qty': 'Qty',
'Backlog row Qty': 'Backlog Qty',
'Remain. crit. Qty': 'Rem. Qty'
})
# Rename certain columns
df_TurnoverReport = df_TurnoverReport.rename(columns={
'TurnoverReport row Qty': 'Qty',
})
# Rename certain columns
df_Summary = df_Summary.rename(columns={
'Remain. crit. Qty': 'Rem. Qty',
'Max Qty (GS)': 'Qty (GS/BOM)'
})
#################################################################################################################
# Widgets initialization
################################################################################################################
#-------------------------------------- UPDATE 04/18 -----------------------------
default_program = 'Phase 4-5'
default_priority = 6
default_indice = 'P6'
# Function to filter priorities based on program selection
def filter_priorities(program):
if program == 'All':
priorities = ['All'] + df_Priority['Priority'].unique().tolist()
else:
priorities = df_Priority[df_Priority['Program'] == program]['Priority'].unique().tolist()
return priorities
# Function to filter indices based on priority selection
def filter_indices(priority):
if priority == 'All':
indices = df_Priority['Pty Indice'].unique().tolist() # Remove 'All' option here
else:
# Filter indices based on the selected priority
indices = df_Priority[df_Priority['Priority'] == priority]['Pty Indice'].unique().tolist()
return indices
''' Updated 03/28
# Initialize program widget, excluding NaN values
unique_programs = df_Priority['Program'].dropna().unique().tolist()
program_widget = pn.widgets.Select(name='Select Program', options=unique_programs, value=default_program)
'''
#---------------- 03/28 ------------------------------
# Explicit list of allowed programs
allowed_programs_4quadrants = ['Phase 4-5', 'EMBRAER', 'SIKORSKY', 'COMAC', '1stTB', 'Space-X'] # 04/17
# Validate default_program - ensure it's never empty/invalid
if (not isinstance(default_program, str) or
default_program.strip() == "" or
default_program not in allowed_programs_4quadrants):
default_program = allowed_programs_4quadrants[0] # Fallback to first valid program
# Create widget with guaranteed non-empty selection
program_widget = pn.widgets.Select(
name='Select Program',
options=allowed_programs_4quadrants,
value=default_program,
)
#----------------------------------------------------
# Initialize priority widgets
filtered_priorities = filter_priorities(default_program)
priority_widget = pn.widgets.Select(name='Select Priority', options=filtered_priorities, value=default_priority)
# Initialize indice widgets
filtered_indices = filter_indices(default_priority)
indice_widget = pn.widgets.Select(name='Select Pty Indice', options=filtered_indices, value=default_indice)
#---------------------------------------------------------------------------------------------
######################################################################################
# Widgets callback functions
########################################################################################
# Callback function to update priority_widget and indice_widget when the program changes
def update_program(event):
selected_program = program_widget.value
# Update priorities based on selected program
priority_widget.options = filter_priorities(selected_program)
# Ensure the selected priority is valid
if priority_widget.value not in priority_widget.options:
priority_widget.value = priority_widget.options[0] if priority_widget.options else None
# Update indices based on the updated priority
update_priorities(event)
# Function to update priorities based on program selection
def update_priorities(event):
selected_program = program_widget.value
updated_priorities = df_Priority[df_Priority['Program'] == selected_program]['Priority'].unique().tolist()
# Update priorities widget options
priority_widget.options = updated_priorities
# Ensure the selected priority is valid
if priority_widget.value not in updated_priorities:
priority_widget.value = updated_priorities[0] if updated_priorities else None
# Update indices based on the updated priority
update_indices(event)
# Function to update indices based on priority selection
def update_indices(event):
selected_priority = priority_widget.value
indice_widget.options = filter_indices(selected_priority)
# Set to default value if it's valid; otherwise, choose the first available
if default_indice in indice_widget.options:
indice_widget.value = default_indice
else:
indice_widget.value = indice_widget.options[0] if indice_widget.options else None
#--------------------------------------------------------------
# Add filter widgets for IDD Top Level and SEDA Top Level
#--------------------------------------------------------------
# Define filtering widgets for 'IDD Top Level' and 'SEDA Top Level'
label_idd_top_level = pn.pane.HTML('<b style="color:#2B70B3;">IDD Top Level Filter</b>')
label_seda_top_level = pn.pane.HTML('<b style="color:#2B70B3;">SEDA Top Level Filter</b>')
# 05/16 update
filters_top_level_4_cadran = {
'IDD Top Level': pn.widgets.TextInput(name='', placeholder='Enter IDD Top Level'),
'SEDA Top Level': pn.widgets.TextInput(name='', placeholder='Enter SEDA Top Level'),
}
# Create buttons for applying filters
filters_top_level_button_4_cadran = pn.widgets.Button(name='Apply Filters', button_type='primary')
# Set default value to None for all filter widgets
for widget in filters_top_level_4_cadran.values():
widget.value = ''
# 05/16 - New code
#--------------------------------------------------------------
# Add Reset widgets for IDD Top Level and SEDA Top Level
#--------------------------------------------------------------
# Create Reset button
reset_button_4_cadran = pn.widgets.Button(name='Reset filters', button_type='danger')
# Define reset callback
def reset_filters_4_cadran(event):
"""
Resets all widgets, including filters_Prod (with DatePickers), to their default values and triggers updates.
"""
# Reset main selection widgets to default values
program_widget.value = default_program
priority_widget.value = default_priority
indice_widget.value = default_indice
# Reset IDD Top Level and SEDA Top Level filters
filters_top_level_4_cadran['IDD Top Level'].value = ''
filters_top_level_4_cadran['SEDA Top Level'].value = ''
# Reset filters_Prod widgets based on their type
for widget in filters_Prod.values():
if isinstance(widget, pn.widgets.TextInput):
widget.value = ''
elif isinstance(widget, pn.widgets.MultiChoice):
widget.value = []
elif isinstance(widget, pn.widgets.Select):
widget.value = widget.options[0] if widget.options else None
elif isinstance(widget, pn.widgets.Checkbox):
widget.value = False
elif isinstance(widget, pn.widgets.DatePicker):
widget.value = None # Clear date fields
# Add other widget types as needed
# Trigger updates to refresh widget options and dashboard
try:
update_program(None) # Update priorities and indices based on default program
update_priorities(None) # Ensure priority options are updated
update_indices(None) # Ensure indice options are updated
update_supply_table_fullArchi(None) # Update Supply Chain tables
update_wip_selected_top(None) # Update Production Dashboard (e.g., wip_table)
except Exception as e:
print(f"Error in updates: {e}")
# Attach callback to reset button
reset_button_4_cadran.on_click(reset_filters_4_cadran)
#--------------------------------------------------------------
# 05/16 - Update with Reset button
# Create the layout with labels, filter widgets, and buttons
filter_widgets_top_level_4_cadran = pn.Row(
pn.Column(label_idd_top_level, filters_top_level_4_cadran['IDD Top Level']),
pn.Column(label_seda_top_level, filters_top_level_4_cadran['SEDA Top Level']),
pn.Column(
pn.Spacer(height=25), # Spacer before the buttons
pn.Row(
filters_top_level_button_4_cadran,
pn.Spacer(width=10), # Space between Apply Filters and Reset buttons
reset_button_4_cadran
)
)
)
#--------------------------------------------------------------
# Create a mapping between IDD Top Level/SEDA Top Level and other widget values
#--------------------------------------------------------------
def create_mapping(df):
mapping = {}
for _, row in df.iterrows():
idd_top_level = row['IDD Top Level']
seda_top_level = row['SEDA Top Level']
program = row['Program']
priority = row['Priority']
pty_indice = row['Pty Indice']
# Map IDD Top Level to Program, Priority, and Pty Indice
mapping[idd_top_level] = {'Program': program, 'Priority': priority, 'Pty Indice': pty_indice}
# Map SEDA Top Level to Program, Priority, and Pty Indice
mapping[seda_top_level] = {'Program': program, 'Priority': priority, 'Pty Indice': pty_indice}
return mapping
# Create the mapping
mapping = create_mapping(df_Priority)
#--------------------------------------------------------------
# Update Widgets Based on IDD Top Level or SEDA Top Level
#--------------------------------------------------------------
# This function will be executed when the "Apply Filters" button is clicked
def apply_filters_button_click(event):
# Get the current widget values
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
idd_top_level_filter = filters_top_level_4_cadran['IDD Top Level'].value
seda_top_level_filter = filters_top_level_4_cadran['SEDA Top Level'].value
# Update widgets based on IDD Top Level or SEDA Top Level filters
if idd_top_level_filter and idd_top_level_filter in mapping:
program_widget.value = mapping[idd_top_level_filter]['Program']
priority_widget.value = mapping[idd_top_level_filter]['Priority']
indice_widget.value = mapping[idd_top_level_filter]['Pty Indice']
elif seda_top_level_filter and seda_top_level_filter in mapping:
program_widget.value = mapping[seda_top_level_filter]['Program']
priority_widget.value = mapping[seda_top_level_filter]['Priority']
indice_widget.value = mapping[seda_top_level_filter]['Pty Indice']
else:
print("No valid input, resetting widgets to default.") # Debugging
program_widget.value = 'All'
priority_widget.value = 'All'
indice_widget.value = 'All'
# Initialize a boolean mask with all True values
mask = pd.Series(True, index=df_Priority.index)
# Apply filters based on selections
if selected_program != 'All':
mask &= (df_Priority['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_Priority['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_Priority['Pty Indice'] == selected_indice)
if idd_top_level_filter:
mask &= (df_Priority['IDD Top Level'].str.contains(idd_top_level_filter, case=False, na=False))
if seda_top_level_filter:
mask &= (df_Priority['SEDA Top Level'].str.contains(seda_top_level_filter, case=False, na=False))
# Apply the mask and return the filtered DataFrame
filtered_df = df_Priority[mask]
# Display or print the filtered DataFrame (for testing)
print(filtered_df) # Or update a display component with the filtered DataFrame
# Link the button click event to the apply_filters_button_click function
filters_top_level_button_4_cadran.on_click(apply_filters_button_click)
#--------------------------------------------------------------
##############################################################################################################################
# --->>>> ENGINEERING <<<---
##############################################################################################################################
# --> Pending Report
##############################################################################################################################
#Get date from the Pending Report - ['Last Update'] in [T2]
date_pendingreport = df_PendingReport['Last Update'].iloc[0] # Get the first date in the 'Last Update' column
# Fill NaN values with empty strings
df_PendingReport['Comment'].fillna('', inplace=True)
# Define the initial empty DataFrame for changes_table
initial_changes_df = pd.DataFrame(columns=['Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Item Number', 'Action Needed', 'Rel Date', 'Comment'])
# Define column widths dictionary
column_widths = {
'Pty Indice': 60
}
# Create a Tabulator widget for sales_table
changes_table = pn.widgets.Tabulator(
initial_changes_df,
layout='fit_data_table', # Adjust columns to fit data (excluding header)
sizing_mode='stretch_width',
show_index=False, # This hides the index column
widths=column_widths # Set column widths
)
# Create a Markdown pane for messages in the Pending Report
pending_message_pane = pn.pane.Markdown("")
def update_changes_table(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Filter df_PendingReport based on selected_program
if selected_program == 'All':
filtered_df_PendingReport = df_PendingReport.copy()
else:
filtered_df_PendingReport = df_PendingReport[df_PendingReport['Program'] == selected_program]
# Apply additional filters
if selected_priority != 'All':
filtered_df_PendingReport = filtered_df_PendingReport[filtered_df_PendingReport['Priority'] == selected_priority]
if selected_indice != 'All':
filtered_df_PendingReport = filtered_df_PendingReport[filtered_df_PendingReport['Pty Indice'] == selected_indice]
# Check if the filtered DataFrame is empty
if filtered_df_PendingReport.empty:
pending_message_pane.object = "**No open changes related to this PN**" # Simple message
changes_table.visible = False # Hide the changes table
else:
changes_table.value = filtered_df_PendingReport[['Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Item Number', 'Action Needed', 'Rel Date', 'Comment']]
pending_message_pane.object = "" # Clear the message
changes_table.visible = True # Show the changes table
# Initialize the changes table with default values
update_changes_table(None)
##############################################################################################################################
# --> ADCN Report
##############################################################################################################################
#Get date from the Pending Report - ['Last Update'] is column [S] wiht the date in S2
date_adcngreport = df_ADCNReport['Last Update'].iloc[0] # Get the first date in the 'Last Update' column
# Reformatting of 'Pty Indice', erase the '[' and ']' and only keep the values inside or the values separated with ';'
df_ADCNReport['Pty Indice'] = df_ADCNReport['Pty Indice'].str.strip('[]').str.split(';')
# Explode the DataFrame to create a new row for each index
df_exploded = df_ADCNReport.explode('Pty Indice')
# Mapping Priority based on 'Pty Indice'
df_exploded['Priority'] = df_exploded['Pty Indice'].map(indice_to_priority)
# Reset the index if needed
df_exploded.reset_index(drop=True, inplace=True)
# Display the final DataFrame
#print('df_ADCNReport after transformation:')
#display(df_exploded[['Pty Indice', 'ADCN#', 'ESR#', 'Created', 'Release Date', 'Drawing Number', 'Status', 'ADCN Rev', 'Change Description', 'Priority', 'Program']])
# Update df_ADCNReport with the exploded DataFrame
df_ADCNReport = df_exploded
#Create a panda datafram with the columns 'Pty Indice', 'ADCN#' , 'ESR#', 'Created', 'Release Date', 'Drawing Number', 'Status', 'ADCN Rev', 'Change Description', 'Priority', 'Program'
df_ADCNReport = df_ADCNReport[['Pty Indice', 'ADCN#', 'ESR#', 'Created', 'Release Date', 'Drawing Number', 'Status', 'ADCN Rev', 'Change Description', 'Priority', 'Program']]
#Replace 'Status'and 'ADCN Rev' with empty space
df_ADCNReport['Status'].fillna('', inplace=True) # Replace NaN with empty string for 'Status'
df_ADCNReport['ADCN Rev'].fillna('', inplace=True) # Replace NaN with empty string for 'ADCN Rev'
df_ADCNReport['Change Description'].fillna('', inplace=True) # Replace NaN with empty string for 'ADCN Rev'
# Convert 'Created' and 'Release Date' to datetime format, stripping time, and formatting as MM/DD/YYYY
df_ADCNReport['Created'] = pd.to_datetime(df_ADCNReport['Created'], errors='coerce')
df_ADCNReport['Release Date'] = pd.to_datetime(df_ADCNReport['Release Date'], errors='coerce')
# Format dates to MM/DD/YYYY
df_ADCNReport['Created'] = df_ADCNReport['Created'].dt.strftime('%m/%d/%Y')
df_ADCNReport['Release Date'] = df_ADCNReport['Release Date'].dt.strftime('%m/%d/%Y')
# Write 'ADCN not created' where 'Created' is empty
df_ADCNReport['Created'].replace('', 'ADCN not created', inplace=True)
df_ADCNReport['Created'].replace(pd.NaT, 'ADCN not created', inplace=True)
# Replace NaN in 'Release Date' with 'Not released'
df_ADCNReport['Release Date'].replace(pd.NaT, 'ADCN not released', inplace=True)
#Display dataframe
#print('df_ADCNReport')
#display(df_ADCNReport)
# Function to apply font color formatting based on 'Status'
def font_color_status(val):
if val == 'ADCN not created':
return 'color: red;' # Return red font color for "ADCN not created"
else:
return 'color: black;' # Default to black font color
# Function to apply font color formatting based on 'Release Date'
def font_color_release_date(val):
if val == 'ADCN not released':
return 'color: red;' # Return red font color for "Not released"
else:
return 'color: black;' # Default to black font color
# Initialize the DataFrame pane and message pane
#ADCN_pane = pn.pane.DataFrame(pd.DataFrame(), sizing_mode='stretch_width')
ADCN_pane = pn.pane.DataFrame(pd.DataFrame(),
sizing_mode='stretch_width', # Keep it responsive
height=600) # Set max height
adcn_message_pane = pn.pane.Markdown("", sizing_mode='stretch_width')
# Update function to handle table updates with color formatting
def update_ADCN_table(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Filter the DataFrame based on selected values
mask = pd.Series(True, index=df_ADCNReport.index)
if selected_priority != 'All':
mask &= (df_ADCNReport['Priority'] == selected_priority)
if selected_program != 'All':
mask &= (df_ADCNReport['Program'] == selected_program)
if selected_indice != 'All':
mask &= df_ADCNReport['Pty Indice'].str.contains(selected_indice)
# Apply the mask to filter the DataFrame
filtered_df = df_ADCNReport[mask]
# Check if the filtered DataFrame is empty
if filtered_df.empty:
ADCN_pane.object = pd.DataFrame() # Clear the DataFrame pane
ADCN_pane.visible = False # Hide the DataFrame pane
adcn_message_pane.object = '**No ADCN related to this PN**' # Show no data message
else:
# Select relevant columns to display and hide 'Priority' and 'Program'
displayed_df = filtered_df[['Pty Indice', 'ADCN#', 'ESR#', 'Created', 'Release Date',
'Drawing Number', 'Status', 'ADCN Rev', 'Change Description']]
# Apply color formatting using 'applymap' for the 'Status' column
#styled_df = displayed_df.style.applymap(font_color_status, subset=['Status'])
# Apply color formatting using 'applymap' for the 'Status' and 'Release Date' columns
styled_df = displayed_df.style.applymap(font_color_status, subset=['Created']) \
.applymap(font_color_release_date, subset=['Release Date'])
# Update the ADCN_pane with the styled DataFrame
ADCN_pane.object = styled_df.hide(axis='index')
ADCN_pane.visible = True # Show the DataFrame pane
adcn_message_pane.object = "" # Clear the message
# Call the update function initially to populate the table
update_ADCN_table(None)
# Attach the update function to widget value changes
program_widget.param.watch(update_ADCN_table, 'value')
priority_widget.param.watch(update_ADCN_table, 'value')
indice_widget.param.watch(update_ADCN_table, 'value')
###############################################################################################
# Initial call to update_widgets_and_table to populate the table based on default selections
#############################################################################################
# Define supply dashboard
changes_dashboard = pn.Column(
pn.pane.HTML(f"""
<div style="text-align: left;">
<style>
h2 {{ margin-bottom: 0; color: #305496; }} /* Set title color here */
p {{ margin-top: 0; }}
</style>
<h2>Engineering</h2>
<p>{f"|PendingReport| - <b>{date_pendingreport}</b>: IDD's internal changes based on Agile (PLM) | Pending Report from Change Analyst | [weekly update]"}</p>
</div>
"""),
pending_message_pane,
changes_table,
pn.Spacer(height=20),
pn.pane.HTML(f"""
<div style="text-align: left;">
<style>
p {{ margin-top: 0; }}
</style>
<p>{f"|ADCN Report| - <b>{date_adcngreport}</b>: IDD's external changes based on SEDA's ADCN Report | Since beginning of the current year | [weekly update]"}</p>
</div>
"""),
adcn_message_pane, # Add ADCN message pane here
ADCN_pane,
sizing_mode='stretch_width' # Adjust sizing mode
)
##############################################################################################################################
# --->>>> SALES <<<--- updated 08/22
##############################################################################################################################
# Backlog
#######################################################
# Define the initial empty DataFrame for sales_table
#initial_sales_df = pd.DataFrame(columns=['Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Qty','Rem. Qty', 'Order', 'Due Date'])
initial_sales_df = pd.DataFrame(columns=['Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Backlog Qty', 'Rem. Qty', 'Order', 'Due Date'])
# Define column widths dictionary
column_widths = {
'Pty Indice': 60,
#'Qty': 60,
'Backlog Qty': 60,
'Rem. Qty': 80,
'Order': 80
}
# Create widget for sales_table
sales_table = pn.pane.DataFrame(
initial_sales_df,
min_height=50,
height=None,
sizing_mode='stretch_width',
index=False # Hides the index column
)
# Create a Markdown pane for messages in the Sales table
message_pane = pn.pane.Markdown("")
def update_sales_table(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Filter df_Backlog based on selected_program
if selected_program == 'All':
filtered_df_backlog = df_Backlog.copy() # Use a copy of the entire DataFrame
else:
filtered_df_backlog = df_Backlog[df_Backlog['Program'] == selected_program]
# Apply additional filters based on selected_priority and selected_indice
if selected_priority != 'All':
filtered_df_backlog = filtered_df_backlog[filtered_df_backlog['Priority'] == selected_priority]
if selected_indice != 'All':
filtered_df_backlog = filtered_df_backlog[filtered_df_backlog['Pty Indice'] == selected_indice]
# Group by 'Order' and aggregate the 'Qty' (sum) and 'Rem. Qty' (first)
aggregated_df = filtered_df_backlog.groupby('Order').agg({
'Pty Indice': 'first',
'IDD Top Level': 'first',
'SEDA Top Level': 'first',
#'Qty': 'sum',
'Backlog Qty': 'sum',
'Rem. Qty': 'first', # Use the first non-null value
'Due Date': 'first',
}).reset_index()
if aggregated_df.empty:
sales_table.object = pd.DataFrame() # Empty DataFrame
message_pane.object = "No data available"
else:
sales_table.object = aggregated_df # Assign new DataFrame
message_pane.object = ""
# Initialize the sales table with default values
update_sales_table(None)
# define color for important text
important_text_color = '#002060' # dark bleu
########################################################
# Create a pane for displaying dynamic text for sales
########################################################
sales_summary = pn.pane.Str(sizing_mode='stretch_width')
def update_sales_summary(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Initialize a boolean mask with all True values
mask = pd.Series(True, index=df_Backlog.index)
# Apply filters based on selections
if selected_program != 'All':
mask &= (df_Backlog['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_Backlog['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_Backlog['Pty Indice'] == selected_indice)
# Filter df_Backlog using the constructed mask
filtered_df_backlog = df_Backlog[mask]
# Check if filtered_df_backlog is empty or not
if filtered_df_backlog.empty:
sales_summary.object = 'No data available'
else:
# Initialize a dictionary to sum quantities for each 'Pty Indice'
summary_dict = {}
# Iterate over each row in filtered_df_backlog
for idx, row in filtered_df_backlog.iterrows():
pty_indice = row['Pty Indice']
order = row['Order']
#qty = row['Qty'] 08/22
qty = row['Backlog Qty']
if pty_indice not in summary_dict:
summary_dict[pty_indice] = {
'total_qty': 0,
'orders': set(),
'shipped': 0,
'critical_qty': 0
} # Initialize orders as a set
summary_dict[pty_indice]['total_qty'] += qty
summary_dict[pty_indice]['orders'].add(order) # Use set to avoid duplicates
# Retrieve shipment data from df_Priority
shipment_data = df_Priority[df_Priority['Pty Indice'].isin(summary_dict.keys())]
# Get details for the current 'Pty Indice'
details = filtered_df_backlog[filtered_df_backlog['Pty Indice'] == pty_indice].iloc[0]
for pty_indice in summary_dict.keys():
# Fetch the shipment data for the current 'Pty Indice'
shipment_info = shipment_data[shipment_data['Pty Indice'] == pty_indice]
# Calculate shipped quantities
shipped = shipment_info['Shipped'].sum() if not shipment_info['Shipped'].isna().all() else 0
#Convert 'shipped' to int - 08/14
shipped = int(shipped)
# Handle and convert 'Critical Qty'
critical_qty = shipment_info['Critical Qty']
if not critical_qty.isna().all():
try:
critical_qty = critical_qty.astype(int).sum()
except ValueError:
critical_qty = 0
else:
critical_qty = 0
# Handle non-numeric values for 'Critical Qty'
if isinstance(critical_qty, str) and critical_qty.strip().lower() == 'completed':
shipment = "Critical quantity fulfilled"
else:
shipment = f"Total quantity (<b>{shipped}</b>) shipped over (<b>{critical_qty}</b>) total critical quantity"
# Add shipment information to the summary
summary_dict[pty_indice]['shipment'] = shipment
# Format the output
lines = []
for pty_indice, data in summary_dict.items():
orders_concat = ', '.join(data['orders']) # Convert set to a sorted list for display
shipment_info = data.get('shipment', 'No shipment information available')
line = (
#f"<u>Pty Indice</u>: <b>{pty_indice}</b> - <b>{details['IDD Top Level']}</b> ({details['SEDA Top Level']})<br>"
f"<u>Pty Indice</u>: <span style='color:{important_text_color};'><b>{pty_indice}</b> - <b>{details['IDD Top Level']}</b></span> ({details['SEDA Top Level']})<br>"
f"▷ IDD Backlog for {pty_indice} is <b>{data['total_qty']}</b> Top-Level within the following SO: {orders_concat}<br>"
f"▷ {shipment_info}<br>"
)
lines.append(line)
# Join all lines into a single string
display_text = '\n'.join(lines)
sales_summary.object = display_text
# Define an initial call to populate the table when the app starts
update_sales_summary(None)
###############################################################
# Turnover Report
###############################################################
# Convert 'Tracking#' to string
df_TurnoverReport['Tracking#'] = df_TurnoverReport['Tracking#'].astype(str)
# Replace 'nan' with an empty string
df_TurnoverReport['Tracking#'] = df_TurnoverReport['Tracking#'].replace('nan', '')
# Remove '.0' from the string values
df_TurnoverReport['Tracking#'] = df_TurnoverReport['Tracking#'].str.replace('.0', '', regex=False)
#Define the initial empty DataFrame for turnover_table
initial_turnover_df = pd.DataFrame(columns=['Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Qty', 'Invoice date', 'Order', 'Tracking#'])
# Ensure 'Invoice date' is in datetime format
df_TurnoverReport['Invoice date'] = pd.to_datetime(df_TurnoverReport['Invoice date'])
# Format 'Invoice date' to short date format
df_TurnoverReport['Invoice date'] = df_TurnoverReport['Invoice date'].dt.strftime('%m/%d/%Y')
# Define column widths dictionary
column_widths = {
'Pty Indice': 60,
'Qty': 60,
'Rem. Qty': 80,
'Order': 80
}
###############################################
# Create a widget for turnover_table
################################################
turnover_table = pn.pane.DataFrame(
initial_turnover_df,
min_height=50,
height=None,
sizing_mode='stretch_width',
index=False # Hides the index column
)
# Define the turnover_message_pane
turnover_message_pane = pn.pane.HTML('')
def update_turnover_table(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Filter df_TurnoverReport based on selected_program
if selected_program == 'All':
filtered_df_TurnoverReport = df_TurnoverReport.copy() # Make a copy of the entire DataFrame
else:
filtered_df_TurnoverReport = df_TurnoverReport[df_TurnoverReport['Program'] == selected_program]
# Apply additional filters based on selected_priority and selected_indice
if selected_priority != 'All':
filtered_df_TurnoverReport = filtered_df_TurnoverReport[filtered_df_TurnoverReport['Priority'] == selected_priority]
if selected_indice != 'All':
filtered_df_TurnoverReport = filtered_df_TurnoverReport[filtered_df_TurnoverReport['Pty Indice'] == selected_indice]
# Check if the filtered DataFrame is empty
if filtered_df_TurnoverReport.empty:
turnover_table.object = pd.DataFrame()
turnover_message_pane.object = f"No shipment or NC received within the period for {selected_priority if selected_indice == 'All' else selected_indice}"
else:
turnover_table.object = filtered_df_TurnoverReport[['Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Qty', 'Invoice date', 'Order', 'Tracking#']]
turnover_message_pane.object = ""
# Initialize the turnover table with default values
update_turnover_table(None)
###############################################
# Create a Markdown pane for turnover summary
################################################
turnover_summary = pn.pane.Str(sizing_mode='stretch_width')
# Convert 'Invoice date' to datetime format
df_TurnoverReport['Invoice date'] = pd.to_datetime(df_TurnoverReport['Invoice date'])
# Calculate the span period of the Turnover Report span_TurnoverReport
start_date = df_TurnoverReport['Invoice date'].min()
end_date = df_TurnoverReport['Invoice date'].max()
span_TurnoverReport = f"{start_date.date()} to {end_date.date()}" # Format dates as needed
def update_turnover_summary(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Filter df_TurnoverReport based on selected_program
if selected_program == 'All':
filtered_df_TurnoverReport = df_TurnoverReport.copy() # Make a copy of the entire DataFrame
else:
filtered_df_TurnoverReport = df_TurnoverReport[df_TurnoverReport['Program'] == selected_program]
# Apply additional filters based on selected_priority and selected_indice
if selected_priority != 'All':
filtered_df_TurnoverReport = filtered_df_TurnoverReport[filtered_df_TurnoverReport['Priority'] == selected_priority]
if selected_indice != 'All':
filtered_df_TurnoverReport = filtered_df_TurnoverReport[filtered_df_TurnoverReport['Pty Indice'] == selected_indice]
# Check if filtered_df_TurnoverReport is empty or not
if filtered_df_TurnoverReport.empty:
turnover_summary.object = 'No data available'
else:
# Initialize a dictionary to sum quantities for each 'Pty Indice'
summary_dict = {}
# Iterate over each row in filtered_df_TurnoverReport
for idx, row in filtered_df_TurnoverReport.iterrows():
pty_indice = row['Pty Indice']
order = row['Order']
qty = row['Qty']
if pty_indice not in summary_dict:
summary_dict[pty_indice] = {
'total_qty': 0,
'top_level_shipped': 0,
'nc_shipped': 0,
'nc_received': 0,
'orders': set(),
'details': filtered_df_TurnoverReport[filtered_df_TurnoverReport['Pty Indice'] == pty_indice].iloc[0]
} # Initialize orders as a set
summary_dict[pty_indice]['total_qty'] += qty
summary_dict[pty_indice]['orders'].add(order) # Use set to avoid duplicates
# Categorize the quantity
if qty > 0:
if 'NC' in order:
summary_dict[pty_indice]['nc_shipped'] += qty
else:
summary_dict[pty_indice]['top_level_shipped'] += qty
elif qty < 0 and 'NC' in order:
summary_dict[pty_indice]['nc_received'] -= qty # Use '-' to keep positive values for display
# Format the output
lines = []
for pty_indice, data in summary_dict.items():
details = data['details']
orders_concat = ', '.join(data['orders'])
line = (
#f"<u>Pty Indice</u>: <b>{pty_indice}</b> - <b>{details['IDD Top Level']}</b> ({details['SEDA Top Level']}) - Repport's span (<b>{span_TurnoverReport}</b>)<br>"
f"<u>Pty Indice</u>: <span style='color:{important_text_color};'><b>{pty_indice}</b> - <b>{details['IDD Top Level']}</b></span> ({details['SEDA Top Level']}) - Repport's span (<b>{span_TurnoverReport}</b>)<br>"
f"▷ Qty Top-Level shipped on the period: <b>{data['top_level_shipped']}</b><br>"
f"▷ Qty NC shipped on the period: <b>{data['nc_shipped']}</b><br>"
f"▷ Qty NC received on the period: <b>{data['nc_received']}</b><br>"
)
lines.append(line)
# Join all lines into a single string
display_text = '<br>'.join(lines)
turnover_summary.object = display_text
# Define an initial call to populate the table when the app starts
update_turnover_summary(None)
###############################################################################################
# Initial call to update_widgets_and_table to populate the table based on default selections
#############################################################################################
sales_dashboard = pn.Column(
pn.pane.HTML(f"""
<div style='text-align: left;'>
<style>
h2 {{ margin-bottom: 0; color: #305496; }} /* Set title color here */
p {{ margin-top: 0; }}
</style>
<h2>Backlog & recent shipment</h2>
<p>{f"|CM-Backlog| - <b>{file_date}</b>: IDD's backlog based on QAD (ERP) | [Daily update]"}</p>
</div>
"""),
pn.Row(sales_summary, sizing_mode='stretch_both'), # Row to stretch content
sales_table,
pn.pane.HTML(f"""
<div style='text-align: left;'>
<p>{f"|CM-Turnover Report| - <b>{file_date}</b>: Top-Level shipped/Received at IDD based on QAD (ERP) | [Daily update, limited span (starts the 1st of current month)]"}</p>
</div>
"""),
turnover_message_pane, # Add the turnover message pane here
turnover_summary,
turnover_table,
sizing_mode='stretch_width', # Adjust sizing mode
height=600 # Set a fixed height to enforce the maximum height
)
##############################################################################################################################
# --->>>> SUPPLY CHAIN <<<---
##############################################################################################################################
# Filter out rows where 'Qty On Hand' is NaN
df_Summary = df_Summary[df_Summary['Qty On Hand'].notna()]
# Round 'Qty On Hand' and 'Qty (GS/BOM)' to integers
df_Summary['Qty On Hand'] = df_Summary['Qty On Hand'].round().astype(int)
#df_Summary['Qty (GS/BOM)'] = df_Summary['Qty (GS/BOM)'].round().astype(int) # saved 02/03
### Update 02/03 #################
# Convert to numeric type first (invalid values become NaN)
df_Summary['Qty (GS/BOM)'] = pd.to_numeric(df_Summary['Qty (GS/BOM)'], errors='coerce')
df_Summary['Max Qty Top-Level'] = pd.to_numeric(df_Summary['Max Qty Top-Level'], errors='coerce') # 03/06
# Handle missing values (fill with 0 or appropriate value)
df_Summary['Qty (GS/BOM)'] = df_Summary['Qty (GS/BOM)'].fillna(0)
# Now perform rounding and conversion to integers
df_Summary['Qty (GS/BOM)'] = df_Summary['Qty (GS/BOM)'].round().astype(int)
####################################
#Display selected_indice, with related 'IDD Top Level', 'SEDA Top Level', 'Top-Level Status' and 'Max Qty Top-Level' from df_Summary above the table
#Apply mapping to create 'Program' column in df_Summary
df_Summary['Program'] = df_Summary['Pty Indice'].map(indice_to_program)
# Fill NaN values with empty strings
df_Summary['Top Level sharing Components'].fillna('', inplace=True)
df_Summary['Comment'].fillna('', inplace=True)
# Replace 'SAFRAN ELEC & DEFENSE(S9412)' with 'Safran EDA' in the 'Supplier' column
df_Summary['Supplier'].replace('SAFRAN ELEC & DEFENSE(S9412)', 'Safran EDA', inplace=True)
# 09/20 update
# Define a list of acronyms to preserve in uppercase
acronyms = ['EDA', 'PCB', 'PWB', 'CPA', 'CPSL', 'ISP', 'TBD'] # Add more acronyms as needed
# Function to capitalize while preserving acronyms
def title_with_acronyms(text, acronyms):
# Convert the text to title case (first letter capitalized, rest lowercase)
text = text.lower().title()
# Use regex to replace the acronyms in uppercase
for acronym in acronyms:
text = re.sub(rf'\b{acronym.title()}\b', acronym, text)
return text
# Apply the function to the 'Supplier' and 'Description' columns
df_Summary['Supplier'] = df_Summary['Supplier'].astype(str) # Added 02/03
df_Summary['Supplier'] = df_Summary['Supplier'].apply(lambda x: title_with_acronyms(x, acronyms))
df_Summary['Description'] = df_Summary['Description'].astype(str) # Added 02/03
df_Summary['Description'] = df_Summary['Description'].apply(lambda x: title_with_acronyms(x, acronyms))
##################################################################
# Create a supply_table with Panel for Purchased architecture
##################################################################
# Color formating of wip_table - ['Level']
###########################################
# Color of 'Level'
#Level == 0: '63BE7B' # Green
#Level == 1: 'A2C075' # Lighter Green
#Level == 2: 'FFEB84' # Yellow
#Level == 3: 'FFD166' # Orange
#Level == 4: 'F88E5B' # Darker Orange
#Level == 5: 'F8696B' # Red
#Level == 6: '8B0000' # Darker Red
# Define color mapping for 'Level'
color_mapping_Level = {
0: '#63BE7B',
1: '#A2C075',
2: '#FFEB84',
3: '#FFD166',
4: '#F88E5B',
5: '#F8696B',
6: '#8B0000'
}
def apply_color_formatting(df):
# Create a DataFrame for styles, initializing with empty strings
styles = pd.DataFrame('', index=df.index, columns=df.columns)
# Fill missing values in 'Level' with a default value or handle them separately
if 'Level' in df.columns:
df['Level'] = df['Level'].fillna(-1) # Using -1 or any other default value that does not conflict with valid levels
for idx, value in df['Level'].items():
if pd.isna(value) or value not in color_mapping_Level:
styles.at[idx, 'Level'] = 'background-color: #FFFFFF' # Default color for missing values
else:
styles.at[idx, 'Level'] = f'background-color: {color_mapping_Level[value]}'
# Apply font and fill styling for 'Qty (GS/BOM)' column - New 09/18
# Apply font, fill, and border styling for 'Qty (GS/BOM)' column
if 'Qty (GS/BOM)' in df.columns:
for idx, value in df['Qty (GS/BOM)'].items():
if value == 0:
styles.at[idx, 'Qty (GS/BOM)'] = (
'color: #C00000; '
'background-color: #FFC7CE; '
'border: 1px dashed #C00000'
)
return styles
# Function to apply color formatting to 'Level' column
def color_levels(val):
color = color_mapping_Level.get(val, '#FFFFFF') # Default to white if no mapping exists
return f'background-color: {color}'
# Update function to handle table updates with color formatting
def update_supply_table(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Filter df_Summary based on selected_program
if selected_program == 'All':
filtered_df_summary = df_Summary.copy() # Make a copy of the entire DataFrame
else:
filtered_df_summary = df_Summary[df_Summary['Program'] == selected_program]
# Apply additional filters based on selected_priority and selected_indice
if selected_priority != 'All':
filtered_df_summary = filtered_df_summary[filtered_df_summary['Priority'] == selected_priority]
if selected_indice != 'All':
filtered_df_summary = filtered_df_summary[filtered_df_summary['Pty Indice'] == selected_indice]
# Filter out rows where 'Supplier' == 'Make Part'
#filtered_df_summary = filtered_df_summary[filtered_df_summary['Supplier'] != 'Make Part']
filtered_df_summary = filtered_df_summary[~filtered_df_summary['Supplier'].str.contains(r'Make Part( CUU)?( \(Phantom\))?', case=False, na=False)] # 03/06
#------------- 03/06 ----------------
# Ensure the 'Critical' column is boolean
# If the column contains strings like 'TRUE' or 'FALSE', convert them to boolean values
if 'Critical' in filtered_df_summary.columns:
filtered_df_summary['Critical'] = filtered_df_summary['Critical'].astype(str).str.strip().str.upper() == 'TRUE'
# Filter out rows where 'Critical' is not True
filtered_df_summary = filtered_df_summary[filtered_df_summary['Critical']]
#----------------------------------------
# Convert 'Level' and 'Rem. Qty' to integer
filtered_df_summary['Level'] = pd.to_numeric(filtered_df_summary['Level'], errors='coerce').fillna(-1).astype(int)
filtered_df_summary['Rem. Qty'] = pd.to_numeric(filtered_df_summary['Rem. Qty'], errors='coerce').fillna(0).astype(int)
# Filter out rows where 'Qty (GS/BOM)' > 'Remain. crit. Qty'
#filtered_df_summary = filtered_df_summary[filtered_df_summary['Qty (GS/BOM)'] <= filtered_df_summary['Rem. Qty']] # 03/06 display only component where 'Qty (GS/BOM)' < 'Max Qty Top-Level' (from 'Level' == 0)
#filtered_df_summary = filtered_df_summary[filtered_df_summary['Qty (GS/BOM)'] <= max_qty_top_level] # 03/06WIP display only component where 'Qty (GS/BOM)' < 'Max Qty Top-Level' (from 'Level' == 0)
# Exclude rows where 'Level' == 0 # 03/06
filtered_df_summary = filtered_df_summary[filtered_df_summary['Level'] != 0]
# Sort by 'Pty Indice' and 'BOM Index'
#filtered_df_summary = filtered_df_summary.sort_values(by=['Pty Indice', 'BOM Index']) #saved 02/03
#filtered_df_summary = filtered_df_summary.sort_values(by=['Pty Indice', 'BOM_Index'])
filtered_df_summary = filtered_df_summary.sort_values(by=['BOM_Index']) # 03/06
# Check if the filtered DataFrame is empty # 02/26
if filtered_df_summary.empty:
supply_table.object = pd.DataFrame({
'Pty Indice': ['No Data'],
'IDD Component': [''],
'Level': [''],
'Description': [''],
'Qty (GS/BOM)': [''],
'Supplier': [''],
#'Top Level sharing Components': [''],
#'Comment': [''],
#'Qty On Hand': [''],
'Max Qty Top-Level': [''], # 03/06
'Rem. Qty': ['']
})
message_pane.object = 'No data available'
else:
#supply_table_df = filtered_df_summary[['Pty Indice', 'IDD Component', 'Level', 'Description', 'Qty (GS/BOM)', 'Supplier', 'Top Level sharing Components', 'Comment', 'Qty On Hand', 'Rem. Qty']]
supply_table_df = filtered_df_summary[['Pty Indice', 'IDD Component', 'Level', 'Description', 'Qty (GS/BOM)', 'Supplier', 'Max Qty Top-Level', 'Rem. Qty']] # 03/06
# Apply color formatting using 'applymap' for the 'Level' column
styled_df = supply_table_df.style.applymap(color_levels, subset=['Level'])
# Update the supply_table with the styled DataFrame
supply_table.object = styled_df.hide(axis='index')
message_pane.object = "" # Clear the message
# Initialize the supply_table pane
supply_table = pn.pane.DataFrame(pd.DataFrame(), sizing_mode='stretch_width')
message_pane = pn.pane.Markdown("", sizing_mode='stretch_width')
def on_widget_change_supply(event):
update_supply_table(event) # Simply call the update function
# Call the update initially to trigger the first load
update_supply_table(None)
############## Update 09/16 ################
# Filters for update_supply_table_fullArchi
#########################################
# Define filtering widgets using HTML panes for labels
label_idd_component = pn.pane.HTML('<b style="color:#2B70B3;">IDD Component Filter</b>')
label_supplier = pn.pane.HTML('<b style="color:#2B70B3;">Supplier Filter</b>')
# Define filtering widgets
filters_fullArchi = {
'IDD Component': pn.widgets.TextInput(name=''),
'Supplier': pn.widgets.TextInput(name=''),
}
# Create a button to trigger filtering
filters_fullArchi_button = pn.widgets.Button(name='Apply Filters', button_type='primary')
reset_fullArchi_button = pn.widgets.Button(name='Reset Filters', button_type='default')
############## Update 09/16 ################
# filters_fullArchi for update_supply_table_fullArchi
#########################################
# Set default value to None for all filter widgets
for widget in filters_fullArchi.values():
widget.value = None
# Create the layout with labels and widgets
filter_widgets_fullArchi = pn.Row(
pn.Column(label_idd_component, filters_fullArchi['IDD Component']),
pn.Column(label_supplier, filters_fullArchi['Supplier']),
pn.Column(
pn.Spacer(height=25), # Spacer before the buttons
pn.Row(filters_fullArchi_button, reset_fullArchi_button)
)
)
# Initial call to populate the table based on default selections
#update_supply_table_fullArchi(None)
#########################################
# Update 09/16
########################################################################################
# Create a supply_table with Panel for full architecture Make and Purchased part
#######################################################################################
# Update WIP 09/27 to apply color mapping when widget is updated
def update_supply_table_fullArchi(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
idd_component_filter = filters_fullArchi['IDD Component'].value
supplier_filter = filters_fullArchi['Supplier'].value
# Filter df_Summary based on selected_program
if selected_program == 'All':
filtered_df_summary = df_Summary.copy() # Make a copy of the entire DataFrame
else:
filtered_df_summary = df_Summary[df_Summary['Program'] == selected_program]
# Apply additional filters based on selected_priority and selected_indice
if selected_priority != 'All':
filtered_df_summary = filtered_df_summary[filtered_df_summary['Priority'] == selected_priority]
if selected_indice != 'All':
filtered_df_summary = filtered_df_summary[filtered_df_summary['Pty Indice'] == selected_indice]
# Apply filters from the new filter widgets for 'IDD Component' and 'Supplier'
if idd_component_filter: # Only apply if a filter value is provided
filtered_df_summary = filtered_df_summary[filtered_df_summary['IDD Component'].str.contains(idd_component_filter, case=False, na=False)]
if supplier_filter: # Only apply if a filter value is provided
filtered_df_summary = filtered_df_summary[filtered_df_summary['Supplier'].str.contains(supplier_filter, case=False, na=False)]
#------------- 03/06 ----------------
# Ensure the 'Critical' column is boolean
# If the column contains strings like 'TRUE' or 'FALSE', convert them to boolean values
if 'Critical' in filtered_df_summary.columns:
filtered_df_summary['Critical'] = filtered_df_summary['Critical'].astype(str).str.strip().str.upper() == 'TRUE'
# Filter out rows where 'Critical' is not True
filtered_df_summary = filtered_df_summary[filtered_df_summary['Critical']]
#----------------------------------------
# Convert 'Level' and 'Rem. Qty' to integer, handling formatting issues
filtered_df_summary['Level'] = pd.to_numeric(filtered_df_summary['Level'], errors='coerce').fillna(-1).astype(int)
filtered_df_summary['Rem. Qty'] = pd.to_numeric(filtered_df_summary['Rem. Qty'], errors='coerce').fillna(0).astype(int)
# Print unique values in 'Level' to debug
#print("Unique values in 'Level' after conversion:", filtered_df_summary['Level'].unique())
# Ensure that -1 is in color_mapping_Level or handle it
if -1 not in color_mapping_Level:
color_mapping_Level[-1] = 'background-color: #FFFFFF' # Default color for -1
# Filter out rows where 'Qty (GS/BOM)' > 'Rem. Qty'
#filtered_df_summary = filtered_df_summary[filtered_df_summary['Qty (GS/BOM)'] <= filtered_df_summary['Rem. Qty']] # 03/06 display only component where 'Qty (GS/BOM)' < 'Max Qty Top-Level'
#filtered_df_summary = filtered_df_summary[filtered_df_summary['Qty (GS/BOM)'] <= filtered_df_summary['Max Qty Top-Level']] # 03/06WIP display only component where 'Qty (GS/BOM)' < 'Max Qty Top-Level'
# Exclude rows where 'Level' == 0 # 03/06
filtered_df_summary = filtered_df_summary[filtered_df_summary['Level'] != 0]
# Sort by 'Pty Indice' and 'BOM Index'
#filtered_df_summary = filtered_df_summary.sort_values(by=['Pty Indice', 'BOM Index']) # saved 02/03
#filtered_df_summary = filtered_df_summary.sort_values(by=['Pty Indice', 'BOM_Index'])
filtered_df_summary = filtered_df_summary.sort_values(by=['BOM_Index']) # 03/06
# Check if the filtered DataFrame is empty # 02/26
if filtered_df_summary.empty:
supply_table_fullArchi.object = pd.DataFrame({
'Pty Indice': ['No Data'],
'IDD Component': [''],
'Level': [''],
'Description': [''],
'Qty (GS/BOM)': [''],
'Supplier': [''],
#'Top Level sharing Components': [''],
#'Comment': [''],
#'Qty On Hand': [''],
'Max Qty Top-Level': [''], # 03/06
'Rem. Qty': ['']
})
message_pane_fullArchi.object = 'No data available' # Display a message indicating no data
else:
#supply_table_df = filtered_df_summary[['Pty Indice', 'IDD Component', 'Level', 'Description', 'Qty (GS/BOM)', 'Supplier', 'Top Level sharing Components', 'Comment', 'Qty On Hand', 'Rem. Qty']]
supply_table_df = filtered_df_summary[['Pty Indice', 'IDD Component', 'Level', 'Description', 'Qty (GS/BOM)', 'Supplier', 'Max Qty Top-Level', 'Rem. Qty']] # 03/06
# Apply color formatting to 'Level' column
styles = apply_color_formatting(supply_table_df)
# Update the supply_table with styled DataFrame
supply_table_fullArchi.object = supply_table_df.style.apply(lambda x: styles.loc[x.name], axis=1).hide(axis='index')
message_pane_fullArchi.object = "" # Clear the message
#### New 09/16 ####
# Define callback function for the button
def on_filter_button_click(event):
update_supply_table_fullArchi(event)
# Define callback function for the Reset Filters button
def on_reset_button_click(event):
# Reset filter values
for widget in filters_fullArchi.values():
widget.value = ""
# Update table with no filters applied
update_supply_table_fullArchi(event)
# Link the buttons to their respective update functions
filters_fullArchi_button.on_click(on_filter_button_click)
reset_fullArchi_button.on_click(on_reset_button_click)
####################
# Initialize the supply_table pane
message_pane_title = pn.pane.Str("▷ List of components, full architecture (<b>Make Part & Purchased parts</b>) to reach the critical quantity:", sizing_mode='stretch_width')
supply_table_fullArchi = pn.pane.DataFrame(pd.DataFrame(), sizing_mode='stretch_width')
message_pane_fullArchi = pn.pane.Markdown("", sizing_mode='stretch_width')
def on_widget_change_supply_fullArchi(event):
update_supply_table_fullArchi(event)
# Initial call to populate the table based on default selections
update_supply_table_fullArchi(None)
###########################################
# Create a widget for supply_selected_top
###########################################
# Create a scrollable pane for displaying text
#supply_selected_top = pn.pane.Str(example_text, height_policy='max', max_height=400, sizing_mode='stretch_width')
# Create a pane for displaying dynamic text (supply_selected_top)
supply_selected_top = pn.pane.Str(sizing_mode='stretch_width')
def update_supply_selected_top(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Initialize a boolean mask with all True values
mask = pd.Series(True, index=df_Snapshot.index)
# Apply filters based on selections
if selected_program != 'All':
mask &= (df_Snapshot['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_Snapshot['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_Snapshot['Pty Indice'] == selected_indice)
# Filter df_Snapshot using the constructed mask
filtered_df_snapshot = df_Snapshot[mask]
# Check if filtered_df_snapshot is empty or not
if filtered_df_snapshot.empty:
# Handle empty DataFrame scenario, e.g., display a message or return early
supply_selected_top.object = 'No data available'
else:
# Initialize an empty list to store formatted strings
lines = []
# Merge with df_Summary to get 'Qty (GS/BOM)' to get 'Qty (GS/BOM)'
merged_df = filtered_df_snapshot.merge(df_Summary[['Pty Indice', 'Qty (GS/BOM)', 'Rem. Qty']], on='Pty Indice', how='left')
# Ensure 'Rem. Qty' column exists and convert it to integers - 08/14
#merged_df['Rem. Qty'] = merged_df['Rem. Qty'].astype(int)
# Handle missing and infinite values in 'Rem. Qty' - Update 09/03
merged_df['Rem. Qty'] = merged_df['Rem. Qty'].replace([float('inf'), -float('inf')], 0)
merged_df['Rem. Qty'] = merged_df['Rem. Qty'].fillna(0)
# Convert the column to integer
merged_df['Rem. Qty'] = merged_df['Rem. Qty'].astype(int)
# Calculate the minimum value from 'Qty (GS/BOM)' in the merged DataFrame
min_qty_gs = merged_df['Qty (GS/BOM)'].min()
# Drop duplicate rows based on 'Pty Indice'
merged_df = merged_df.drop_duplicates(subset=['Pty Indice'])
# Function to determine the color based on 'Top-Level Status' - Update 09/16 with Top-Level status 'Completed - No Backlog'
def get_status_color(status):
if status == 'Clear-to-Build':
return 'green'
elif status == 'Short':
return 'red'
else:
return 'black'
# Iterate over each row in merged_df
for idx, row in merged_df.iterrows():
status_color = get_status_color(row['Top-Level Status'])
# Format the line for display with color coding
line = (
#f"<u>Pty Indice</u>: <b>{row['Pty Indice']}</b> - <b>{row['IDD Top Level']}</b> ({row['SEDA Top Level']})<br>"
f"<u>Pty Indice</u>: <span style='color:{important_text_color};'><b>{row['Pty Indice']}</b> - <b>{row['IDD Top Level']}</b></span> ({row['SEDA Top Level']})<br>"
f"▷ Top Level Status: <b style='color:{status_color};'>{row['Top-Level Status']}</b><br>"
f"▷ Qty of {row['Pty Indice']} Top-Level clear to build based on Purchased Part: <b>{row['Qty clear to build']}</b><br>"
f"▷ Qty of {row['Pty Indice']} Top-Level clear to be released based on Make Part: <b>{min_qty_gs}</b><br>"
f"▷ List of components (<b>Purchased only</b>) missing at IDD to reach the critical quantity (<b>{row['Rem. Qty']}</b>) of {row['Pty Indice']}: <br>"
)
# Append the formatted line to lines list
lines.append(line)
# Join all lines into a single string with double newlines between entries
display_text = '\n'.join(lines)
# Update supply_selected_top with the formatted display_text
supply_selected_top.object = display_text
# Define an initial call to populate the table when the app starts
update_supply_selected_top(None)
#############################################################################################
# Initial call to update_widgets_and_table to populate the table based on default selections
#############################################################################################
# Define supply dashboard
supply_dashboard = pn.Column(
pn.pane.HTML(f"""
<div style="text-align: left;">
<style>
h2 {{ margin-bottom: 0; color: #305496; }} /* Set title color here */
p {{ margin-top: 0; }}
</style>
<h2>Supply Chain</h2>
<p>{f"|Summary| - <b>{file_date}</b>: IDD's inventory status based on QAD (ERP) | [Daily update]"}</p>
</div>
"""),
supply_selected_top,
supply_table,
pn.Spacer(height=20),
pn.Column(message_pane_title, filter_widgets_fullArchi, supply_table_fullArchi), # Encapsulate title and table
sizing_mode='stretch_width', # Adjust sizing mode
#height=600 # Set a fixed height to enforce the maximum height
)
##############################################################################################################################
# --->>>> PRODUCTION <<<---
##############################################################################################################################
# Apply mapping to create 'Program' column in df_WIP
df_WIP['Program'] = df_WIP['Pty Indice'].map(indice_to_program)
# Convert Priority in df_WIP to numeric, coercing errors to NaN
df_WIP['Priority'] = pd.to_numeric(df_WIP['Priority'], errors='coerce')
###########################################
# Create a widget for wip_selected_top
###########################################
# Create a pane for displaying dynamic text (supply_selected_top)
#wip_selected_top = pn.pane.Markdown(sizing_mode='stretch_width') -- text style is different
# Create a pane for displaying dynamic text (supply_selected_top)
wip_selected_top = pn.pane.Str(sizing_mode='stretch_width')
# Define constants for sizing
default_max_height = 200 # Max height for the text pane
max_table_height = 300 # Max height for the table
total_height = 600 # Total height for the layout
row_height = 20 # Adjust row height as needed
# Create a pane for displaying dynamic text with scrolling
#wip_selected_top = pn.pane.Markdown('', sizing_mode='stretch_width')
###########################################
# Create a function for wip_selected_top
###########################################
# Function to split long text into multiple lines
def split_long_text(text, max_length):
words = text.split(', ')
lines = []
current_line = ''
for word in words:
if len(current_line) + len(word) + 2 > max_length: # +2 for ", "
lines.append(current_line)
current_line = word
else:
if current_line:
current_line += ', '
current_line += word
lines.append(current_line)
return '\n'.join(lines)
def update_wip_selected_top(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Convert 'Priority' and 'Level' to numeric, coercing errors to NaN
df_WIP['Priority'] = pd.to_numeric(df_WIP['Priority'], errors='coerce')
df_WIP['Level'] = pd.to_numeric(df_WIP['Level'], errors='coerce')
# Apply filters
mask = pd.Series(True, index=df_WIP.index)
if selected_program != 'All':
mask &= (df_WIP['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_WIP['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_WIP['Pty Indice'] == selected_indice)
# Create a copy of the filtered DataFrame to avoid SettingWithCopyWarning
filtered_df_wip = df_WIP[mask].copy()
# Check if any data is available
if filtered_df_wip.empty:
wip_selected_top.object = 'No data available for the selected filters.'
else:
# Forward fill missing values using .loc
filtered_df_wip['WO'] = filtered_df_wip['WO'].fillna(method='ffill')
# Use .loc for forward filling within groups
change_indices = filtered_df_wip.index.to_series().diff().ne(0).cumsum()
filtered_df_wip['WO'] = filtered_df_wip.groupby(change_indices)['WO'].transform(lambda x: x.ffill())
lines = []
filtered_df_wip = filtered_df_wip.drop_duplicates(subset=['Pty Indice', 'WO'])
# Handle cases where 'qty_top_level' or 'qty_sub_level' might be NaN
qty_top_level = filtered_df_wip[filtered_df_wip['Level'] == 0].groupby('Pty Indice')['WO Qty'].sum()
qty_sub_level = filtered_df_wip[filtered_df_wip['Level'] > 0].groupby('Pty Indice')['WO Qty'].sum()
summary_df = pd.DataFrame({
'qty_top_level': qty_top_level,
'qty_sub_level': qty_sub_level
}).reset_index()
for _, row in summary_df.iterrows():
pty_indice = row['Pty Indice']
# Ensure the values are not NaN before converting to int
qty_top_level_value = int(row['qty_top_level']) if pd.notna(row['qty_top_level']) and str(row['qty_top_level']).isdigit() else 0
qty_sub_level_value = int(row['qty_sub_level']) if pd.notna(row['qty_sub_level']) and str(row['qty_sub_level']).isdigit() else 0
# Get details for the current 'Pty Indice'
details = filtered_df_wip[filtered_df_wip['Pty Indice'] == pty_indice].iloc[0]
# Filter work orders for top and sub levels
top_level_df = filtered_df_wip[filtered_df_wip['Level'] == 0]
sub_level_df = filtered_df_wip[filtered_df_wip['Level'] > 0]
# Get unique work orders for top level and sub level, and filter out NaNs
unique_wo_top = top_level_df[top_level_df['Pty Indice'] == pty_indice]['WO'].astype(str).unique()
unique_wo_sub = sub_level_df[sub_level_df['Pty Indice'] == pty_indice]['WO'].astype(str).unique()
# Convert arrays to lists
list_wo_top = ', '.join(unique_wo_top) if unique_wo_top.size > 0 else 'None'
list_wo_sub = ', '.join(unique_wo_sub) if unique_wo_sub.size > 0 else 'None'
# Split long text into multiple lines
list_wo_top = split_long_text(list_wo_top, max_length=120) # Adjust max_length as needed
list_wo_sub = split_long_text(list_wo_sub, max_length=120) # Adjust max_length as needed
# Calculate the number of unique work orders and components
unique_wo_qty_top = len(unique_wo_top)
unique_wo_qty_sub = len(unique_wo_sub)
unique_sub_PN = sub_level_df[sub_level_df['Pty Indice'] == pty_indice]['IDD Component'].nunique()
# Construct the line for output based on availability
top_level_info = (
f"▷ Quantity <b><span style='color:{important_text_color};'>Top-Level</span></b> {pty_indice} on the floor: <b>{qty_top_level_value}</b> Top-Level within <b>{unique_wo_qty_top}</b> WO:<br> {list_wo_top}"
if qty_top_level_value > 0 else f"▷ Quantity {pty_indice} Top Level on the floor: No Top-Level on the floor"
)
sub_level_info = (
f"▷ Quantity of {pty_indice}'s related <b><span style='color:{important_text_color};'>Sub-Level</span></b> on the floor</b>: Total of <b>{qty_sub_level_value}</b> Sub-Level, including <b>{unique_sub_PN}</b> unique components within <b>{unique_wo_qty_sub}</b> WO:\n{list_wo_sub}"
if qty_sub_level_value > 0 else f"▷ Quantity of related {pty_indice} sub-Level on the floor: No Sub-Level on the floor"
)
line = (
#f"<u>Pty Indice</u>: <b>{pty_indice}</b> - <b>{details['IDD Top Level']}</b> ({details['SEDA Top Level']})<br>"
f"<u>Pty Indice</u>: <span style='color:{important_text_color};'><b>{pty_indice}</b> - <b>{details['IDD Top Level']}</b></span> ({details['SEDA Top Level']})<br>"
f"{top_level_info}<br>"
f"{sub_level_info}<br>"
)
lines.append(line)
display_text = '\n'.join(lines)
wip_selected_top.object = display_text
# Define an initial call to populate the table when the app starts
update_wip_selected_top(None)
#########################################################
# Define filtering widgets for filters_Prod
#########################################################
label_wo = pn.pane.HTML('<b style="color:#2B70B3;">WO Filter</b>')
label_idd_component = pn.pane.HTML('<b style="color:#2B70B3;">IDD Component Filter</b>')
filters_Prod = {
'WO': pn.widgets.TextInput(name=''),
'IDD Component': pn.widgets.TextInput(name=''),
}
# Create buttons for applying and resetting filters
filters_Prod_button = pn.widgets.Button(name='Apply Filters', button_type='primary')
reset_Prod_button = pn.widgets.Button(name='Reset Filters', button_type='default')
# Set default value to None for all filter widgets
for widget in filters_Prod.values():
widget.value = ''
# Create the layout with labels, filter widgets, and buttons
filter_widgets_Prod = pn.Row(
pn.Column(label_wo, filters_Prod['WO']),
pn.Column(label_idd_component, filters_Prod['IDD Component']),
pn.Column(
pn.Spacer(height=25), # Spacer before the buttons
pn.Row(filters_Prod_button, reset_Prod_button)
)
)
############################################
# Create a function for wip_table
############################################
# Update 04/18
def update_wip_table():
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
wo_filter = filters_Prod['WO'].value
idd_component_filter = filters_Prod['IDD Component'].value
# Apply filters to df_WIP
mask = pd.Series(True, index=df_WIP.index)
if selected_program != 'All':
mask &= (df_WIP['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_WIP['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_WIP['Pty Indice'] == selected_indice)
if wo_filter:
mask &= (df_WIP['WO'].str.contains(wo_filter, case=False, na=False))
if idd_component_filter:
mask &= (df_WIP['IDD Component'].str.contains(idd_component_filter, case=False, na=False))
filtered_df_wip = df_WIP.loc[mask].copy()
if filtered_df_wip.empty:
# Display a placeholder message if no data is available
wip_table.object = pd.DataFrame({
'Pty Indice': ['No Data'],
'WO': [''],
'WO Qty': [''],
'Last movement': [''],
'Area': [''],
'IDD Component': [''],
'Level': [''],
'Description Component': [''],
'Release': [''],
'Site': [''], # 04/18
'BOM Index': ['']
})
message_pane.object = 'No data available'
else:
# Date processing
filtered_df_wip['Last movement'] = pd.to_datetime(filtered_df_wip['Last movement'], errors='coerce')
filtered_df_wip['Release'] = pd.to_datetime(filtered_df_wip['Release'], errors='coerce')
filtered_df_wip['Last movement'] = filtered_df_wip['Last movement'].fillna(pd.NaT)
filtered_df_wip['Release'] = filtered_df_wip['Release'].fillna(pd.NaT)
#------------------------ 04/18 ----------------------------------------
#Remove rows where 'Last movement' is NA
#filtered_df_wip = filtered_df_wip.dropna(subset=['Last movement'])
# Ensure there are valid rows to process
if filtered_df_wip.empty:
print("No valid data to display in WIP table.")
return # Or update the UI to show an empty table
#--------------------------- 04/21 ---------------------------------------
# Change 'Site' == 100 with 'RED' and 150 with 'CUU'
filtered_df_wip['Site'] = filtered_df_wip['Site'].replace({100: 'RED', 150: 'CUU'})
#-------------------------------------------------------------------------
# Group by 'WO' and select most recent 'Last movement'
def select_most_recent(group):
if group['Last movement'].notna().any():
return group.loc[group['Last movement'].idxmax()]
else:
return group.iloc[0] # fallback: return the first row if all are NaT
filtered_df_wip = filtered_df_wip.groupby('WO').apply(select_most_recent).reset_index(drop=True)
filtered_df_wip = filtered_df_wip.sort_values(by='Release')
# Format dates: convert NaT to empty string for display
filtered_df_wip['Last movement'] = filtered_df_wip['Last movement'].apply(
lambda x: x.strftime('%m-%d-%Y') if pd.notna(x) else ''
)
filtered_df_wip['Release'] = filtered_df_wip['Release'].apply(
lambda x: x.strftime('%m-%d-%Y') if pd.notna(x) else ''
)
# Sort and clean DataFrame
filtered_df_wip = filtered_df_wip[['Pty Indice', 'WO', 'WO Qty', 'Last movement', 'Area', 'IDD Component', 'Level', 'Description Component', 'Release', 'Site', 'BOM Index']]
filtered_df_wip['Level'] = filtered_df_wip['Level'].fillna(-1)
filtered_df_wip = filtered_df_wip.sort_values(by='BOM Index') # Sort only by BOM Index
filtered_df_wip = filtered_df_wip.drop(columns=['BOM Index'])
# Apply color formatting
styles = apply_color_formatting(filtered_df_wip) # Ensure this function returns a valid DataFrame
styled_df = filtered_df_wip.style.apply(lambda x: styles.loc[x.name], axis=1).hide(axis='index')
wip_table.object = styled_df
message_pane.object = ""
# Initialize the wip_table pane with an empty DataFrame
wip_table = pn.pane.DataFrame(
pd.DataFrame(columns=df_WIP.columns),
sizing_mode='stretch_width',
height=500
)
message_pane = pn.pane.Markdown("", sizing_mode='stretch_width')
# Initial call to populate the table based on default selections
update_wip_table()
##########################################################
# Define callback function for the Apply Filters button
###########################################################
# Define callback functions for the buttons
def on_filter_button_click_Prod(event):
update_wip_table()
def on_reset_button_click_prod(event):
for widget in filters_Prod.values():
widget.value = ""
update_wip_table()
filters_Prod_button.on_click(on_filter_button_click_Prod)
reset_Prod_button.on_click(on_reset_button_click_prod)
# Set up callbacks for all widgets
def widget_change_prod(event):
update_wip_table()
##########################################################
#******#########################*******########################*********************************
#########################################################################################################################
# Create Graph 13bis - Combinaison (side by side) of Graph 13 and 13b from the tab |General Overview|
#******#########################*******########################*******************************##########################
def create_placeholder_plot(message):
# Create an empty figure
p = figure(height=250, width=400, title=message)
p.xaxis.visible = False
p.yaxis.visible = False
p.grid.visible = False
p.add_layout(Title(text=message, align='center', text_font_size='10pt', text_color="#002570"), 'above')
return p
########################################################################################################
#Copying pivot table for graphs 13bis and 13bbis and sort by Pty Indice to get PXA before PXB etc.
pivot_table_combined_2 = pivot_table_combined.copy()
#print('pivot_table_combined_2')
#display(pivot_table_combined_2)
# Sort pivot_table_combined_2 by 'Pty Indice'
pivot_table_combined_2 = pivot_table_combined_2.sort_values(by='Pty Indice')
#print('pivot_table_combined_2')
#display(pivot_table_combined_2)
# Mapping on pivot_table_combined to get program
pivot_table_combined_2['Program'] = pivot_table_combined_2['Pty Indice'].map(indice_to_program)
###################################################
# Palette for color of bars and y labels - Order does not matter
###################################################
custom_palette13bis = {"Standard Order":"#A08EBC",
"DPAS Order": "#E4DFEC",
"Qty clear to build": "#7FDB91",
"Qty WIP": "#DAEEF3"}
custom_palette13bbis = {"Total Critical Qty": "#FFA07A",
"Qty Shipped": "#5AB2CA",
"Remain. crit. Qty": "#778899",
"IDD Backlog Qty": "#cdbedd"}
#//////////////////////////////////////////////////
###################################################
# create_plot_13bis
###################################################
def create_plot_13bis():
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
mask = pd.Series(True, index=pivot_table_combined_2.index)
if selected_program != 'All':
mask &= (pivot_table_combined_2['Program'] == selected_program)
if selected_priority != 'All':
mask &= (pivot_table_combined_2['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (pivot_table_combined_2['Pty Indice'] == selected_indice)
filtered_df = pivot_table_combined_2.loc[mask].copy()
#---------- 03/07 - Handled 'No data' in 'Qty clear to build' -----------------------------------------
# Replace 'No data' with NaN in the 'Qty clear to build' column
filtered_df['Qty clear to build'] = filtered_df['Qty clear to build'].replace('No data', np.nan)
# Convert 'Qty clear to build' to numeric (if not already done)
filtered_df['Qty clear to build'] = pd.to_numeric(filtered_df['Qty clear to build'], errors='coerce')
# Optionally, fill NaN with 0 (or another value)
filtered_df['Qty clear to build'] = filtered_df['Qty clear to build'].fillna(0)
#--------------------------------------------------------------------------------------------------------
#####################################################################
# Dynamic y bounds for create_plot_13bis
#####################################################################
if filtered_df.empty:
return create_placeholder_plot('No data available for the selected filters.')
max_quantity_value13bis = filtered_df[['Qty WIP', 'DPAS Order', 'Standard Order' , 'Qty clear to build']].max().max()
min_quantity_value13bis = filtered_df[['Qty WIP', 'DPAS Order', 'Standard Order', 'Qty clear to build']].min().min()
max_y_bound13bis = max_quantity_value13bis * 2
min_y_bound13bis = min_quantity_value13bis * 2 if min_quantity_value13bis < 0 else 0
#Keep order of pivot_table_combined: 'Standard Order', 'DPAS Order', 'Qty WIP', 'Qty clear to build'
# Melt the dataframe --> Reverse order of the bars on the graph
melted_df = filtered_df.melt(id_vars=['Pty Indice'],
value_vars=['Qty WIP', 'Qty clear to build', 'Standard Order', 'DPAS Order'],
var_name='Quantity Type', value_name='Quantity Value')
#############################################
# Set Order of melted_df --> Order of y label
#############################################
# Define the order of categories for 'Quantity Type'
unique_quantity_types = ['DPAS Order', 'Standard Order', 'Qty clear to build', 'Qty WIP']
# Convert 'Quantity Type' to a categorical type with the defined order
melted_df['Quantity Type'] = pd.Categorical(melted_df['Quantity Type'], categories=unique_quantity_types, ordered=True)
##################################################
# Define unique indices and calculate x_combined
##################################################
# New code 08/07
# Define constants
unique_indices = melted_df['Pty Indice'].astype('category').cat.categories
unique_quantity_types = melted_df['Quantity Type'].astype('category').cat.categories
num_types = len(unique_quantity_types) # Number of bars per 'Pty Indice'
num_indice = len(unique_indices) # Number of selected 'Pty Indice'
# Generate base_positions based on enumerate(unique_indices)
base_positions = {indice: i * (num_indice + 1) for i, indice in enumerate(unique_indices)}
# Define gaps -- The gap is supposed to change based on the number of num_indice
def get_gap(num_indice):
# Define a mapping of num_indice to gap values
gap_map = {
3: 0.75,
4: 0.5,
5: 0.33, # Example value, adjust as needed
6: 0.25, # Example value, adjust as needed
8: 0.175, # Example value, adjust as needed
9: 0.125 # Example value, adjust as needed
}
# Return the gap based on the number of indices
return gap_map.get(num_indice, 0.5) # Default to 0.5 if num_indice is not found
# Get Gap
gap = get_gap(num_indice)
# Create a mapping of 'Pty Indice' to its index, starting from 0
indice_mapping = {indice: i for i, indice in enumerate(unique_indices)}
# Calculate x_combined for the bar positions
def calculate_x_combined(row):
pty_indice = row['Pty Indice']
quantity_type_code = melted_df['Quantity Type'].cat.codes[row.name]
# Get the index of the current 'Pty Indice'
indice = indice_mapping[pty_indice]
# Calculate x_combined
x_combined = (base_positions[pty_indice]
+ quantity_type_code
+ 1 / (num_indice + 1) # Small offset to separate bars
+ gap * indice) # Adjust for the gap
# Optionally, print debug information
# print(f"Pty Indice: {pty_indice}, Quantity Type Code: {quantity_type_code}, base position: {base_positions[pty_indice]}, x_combined: {x_combined}")
return x_combined
# Apply the function to calculate x_combined
melted_df['x_combined'] = melted_df.apply(calculate_x_combined, axis=1)
# To inspect the result
#print(melted_df[['Pty Indice', 'Quantity Type', 'x_combined']])
###################
# Create the plot
##################
plot = melted_df.hvplot.bar(
x='Pty Indice',
y='Quantity Value',
by='Quantity Type',
color='Quantity Type',
cmap=custom_palette13bis,
#title='IDD Type of order (DPAS/Standard), Qty Clear-to-Build & Qty WIP per Pty Indice',
xlabel='Pty Indice',
ylabel='Top-Level [Quantity]',
legend='top_right',
stacked=False,
bar_width=0.6, # Set bar width - 09/12
tools=[],
).opts(
xrotation=90,
)
updated_bokeh_plot = hv.render(plot, backend='bokeh')
updated_bokeh_plot.tools = [tool for tool in updated_bokeh_plot.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Pty Indice", "@Pty_Indice"),
("KPI", "@color"),
("Value", "@Quantity_Value"),
]
updated_bokeh_plot.add_tools(hover)
# 09/12 - Set wheel woom inactive
updated_bokeh_plot.toolbar.active_scroll = None
# Customizations
updated_bokeh_plot.xaxis.major_label_text_font_size = '0pt'
updated_bokeh_plot.yaxis.major_label_text_font_size = '10pt'
#updated_bokeh_plot.title.text_font_size = '8pt'
#updated_bokeh_plot.title.text_color = "#002570"
updated_bokeh_plot.xaxis.axis_line_width = 2
updated_bokeh_plot.yaxis.axis_line_width = 2
updated_bokeh_plot.xaxis.major_label_orientation = 'vertical'
updated_bokeh_plot.yaxis.major_label_orientation = 'horizontal'
updated_bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
updated_bokeh_plot.xgrid.grid_line_color = None
updated_bokeh_plot.ygrid.grid_line_color = '#F2F2F2'
updated_bokeh_plot.ygrid.grid_line_dash = [6, 4]
updated_bokeh_plot.y_range = Range1d(start=min_y_bound13bis, end=max_y_bound13bis)
updated_bokeh_plot.toolbar.logo = None
updated_bokeh_plot.legend.label_text_font_size = '8pt'
# Add custom formatted title
updated_bokeh_plot.add_layout(Title(
text="Backlog KPI#1",
align='center',
text_font_size='10pt', # Adjust font size for the title
text_color="#002570" # Adjust color for the title
), 'above')
# Add labels on top of the bars
source = ColumnDataSource(melted_df)
labels = LabelSet(
x= 'x_combined',
#x= 'Quantity Type',
y='Quantity Value',
text='Quantity Value',
level='glyph',
source=source,
text_font_size='8pt',
text_font_style='bold', # Set the font style to bold
text_align='center',
text_baseline='bottom', # Place labels above the bars
y_offset= 5, # Dynamically set the offset based on Quantity Value,
text_color={'field': 'Quantity Type', 'transform': CategoricalColorMapper(
factors=unique_quantity_types, palette=[custom_palette13bis[qtype] for qtype in unique_quantity_types]
)}
)
updated_bokeh_plot.add_layout(labels)
# Debugging: Output the calculated x_combined and other columns
#print("Values in melted_df with x_combined:")
#print(melted_df[['Pty Indice', 'Quantity Type', 'Quantity Value', 'x_combined']])
return updated_bokeh_plot
#//////////////////////////////////////////////////
###################################################
# create_plot_13bbis
###################################################
def create_plot_13bbis():
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
mask = pd.Series(True, index=pivot_table_combined_2.index)
if selected_program != 'All':
mask &= (pivot_table_combined_2['Program'] == selected_program)
if selected_priority != 'All':
mask &= (pivot_table_combined_2['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (pivot_table_combined_2['Pty Indice'] == selected_indice)
filtered_df = pivot_table_combined_2.loc[mask].copy()
#####################################################################
# Dynamic y bounds for create_plot_13bbis
#####################################################################
if filtered_df.empty:
return create_placeholder_plot('No data available for the selected filters.')
max_quantity_value13bbis = filtered_df[['Total Critical Qty', 'Qty Shipped', 'Remain. crit. Qty', 'IDD Backlog Qty']].max().max()
min_quantity_value13bbis = filtered_df[['Total Critical Qty', 'Qty Shipped', 'Remain. crit. Qty', 'IDD Backlog Qty']].min().min()
max_y_bound13bbis = max_quantity_value13bbis * 2
min_y_bound13bbis = min_quantity_value13bbis * 2 if min_quantity_value13bbis < 0 else 0
# keep order of pivot_table_combined: 'Total Critical Qty', 'Qty Shipped', 'Remain. crit. Qty', 'IDD Backlog Qty'
melted_df = filtered_df.melt(id_vars=['Pty Indice'],
value_vars=['Total Critical Qty', 'Qty Shipped', 'Remain. crit. Qty', 'IDD Backlog Qty'],
var_name='Quantity Type', value_name='Quantity Value')
#############################################
# Set Order of melted_df --> Order of y label
#############################################
# Define the order of categories for 'Quantity Type'
unique_quantity_types = ['IDD Backlog Qty', 'Remain. crit. Qty', 'Qty Shipped', 'Total Critical Qty']
# Convert 'Quantity Type' to a categorical type with the defined order
melted_df['Quantity Type'] = pd.Categorical(melted_df['Quantity Type'], categories=unique_quantity_types, ordered=True)
####################################################
# Define unique indices and calculate x_combined
##################################################
# New code 08/07
# Define constants
unique_indices = melted_df['Pty Indice'].astype('category').cat.categories
unique_quantity_types = melted_df['Quantity Type'].astype('category').cat.categories
num_types = len(unique_quantity_types) # Number of bars per 'Pty Indice'
num_indice = len(unique_indices) # Number of selected 'Pty Indice'
# Generate base_positions based on enumerate(unique_indices)
base_positions = {indice: i * (num_indice + 1) for i, indice in enumerate(unique_indices)}
#define gaps -- The gap is suppose to change based on the number of num_indice (for num_indice = 4 -- gap = 0.5 works well)
#gap = 0.5
gap = 1/(num_indice/2)
# Create a mapping of 'Pty Indice' to its index, starting from 0
indice_mapping = {indice: i for i, indice in enumerate(unique_indices)}
# Calculate x_combined for the bar positions
def calculate_x_combined(row):
pty_indice = row['Pty Indice']
quantity_type_code = melted_df['Quantity Type'].cat.codes[row.name]
# Get the index of the current 'Pty Indice'
indice = indice_mapping[pty_indice]
x_combined = base_positions[pty_indice] + quantity_type_code + 1/(num_indice + 1) + gap*indice #Calculation of the gap is yet to be refined as it does not work for all cases when Pty indice > 4
return x_combined
# Apply the function to calculate x_combined
melted_df['x_combined'] = melted_df.apply(calculate_x_combined, axis=1)
#####################################################
plot = melted_df.hvplot.bar(
x='Pty Indice',
y='Quantity Value',
by='Quantity Type',
color='Quantity Type',
cmap=custom_palette13bbis,
#title='<div style="text-align: center;">IDD Total Backlog, Total Critical Quantity,<br> Qty Shipped & Remaining Critical Qty per Pty Indice</div>',
xlabel='Pty Indice',
ylabel='Top-Level [Quantity]',
legend='top_right',
stacked=False,
bar_width=0.6, # Set bar width - 09/12
tools=[],
).opts(
xrotation=90,
)
updated_bokeh_plot = hv.render(plot, backend='bokeh')
updated_bokeh_plot.tools = [tool for tool in updated_bokeh_plot.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Pty Indice", "@Pty_Indice"),
("KPI", "@color"),
("Value", "@Quantity_Value"),
]
updated_bokeh_plot.add_tools(hover)
# 09/12 - Set wheel woom inactive
updated_bokeh_plot.toolbar.active_scroll = None
updated_bokeh_plot.xaxis.major_label_text_font_size = '0pt'
updated_bokeh_plot.yaxis.major_label_text_font_size = '10pt'
#updated_bokeh_plot.title.text_font_size = '8pt'
#updated_bokeh_plot.title.text_color = "#002570"
updated_bokeh_plot.xaxis.axis_line_width = 2
updated_bokeh_plot.yaxis.axis_line_width = 2
updated_bokeh_plot.xaxis.major_label_orientation = 'vertical'
updated_bokeh_plot.yaxis.major_label_orientation = 'horizontal'
updated_bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
updated_bokeh_plot.xgrid.grid_line_color = None
updated_bokeh_plot.ygrid.grid_line_color = '#F2F2F2'
updated_bokeh_plot.ygrid.grid_line_dash = [6, 4]
updated_bokeh_plot.y_range = Range1d(start=min_y_bound13bbis, end=max_y_bound13bbis)
updated_bokeh_plot.toolbar.logo = None
updated_bokeh_plot.legend.label_text_font_size = '8pt' # Set the font size of legend text
# Add custom formatted title
updated_bokeh_plot.add_layout(Title(
text="Backlog KPI#2",
align='center',
text_font_size='10pt', # Adjust font size for the title
text_color="#002570" # Adjust color for the title
), 'above')
# Add labels on top of the bars
source = ColumnDataSource(melted_df)
labels = LabelSet(
x= 'x_combined',
#x= 'Quantity Type',
y='Quantity Value',
text='Quantity Value',
level='glyph',
source=source,
text_font_size='8pt',
text_font_style='bold', # Set the font style to bold
text_align='center',
text_baseline='bottom', # Place labels above the bars
y_offset=5,
text_color={'field': 'Quantity Type', 'transform': CategoricalColorMapper(
factors=unique_quantity_types, palette=[custom_palette13bbis[qtype] for qtype in unique_quantity_types]
)}
)
updated_bokeh_plot.add_layout(labels)
return updated_bokeh_plot
#############
# Inital call
#############
plot_pane_13bis = pn.pane.Bokeh(create_plot_13bis())
plot_pane_13bbis = pn.pane.Bokeh(create_plot_13bbis())
################################################################
# Update methods to include messages when no data is available
###############################################################
def update_plot_13bis(event):
plot_pane_13bis.object = create_plot_13bis()
def update_plot_13bbis(event):
plot_pane_13bbis.object = create_plot_13bbis()
#########################################################################################################################
# Create Graph 14-14b - Combinaison (side by side) of Graph 14 and 14b from the tab |General Overview|
#******#########################*******########################*******************************##########################
#Copying pivot table for graphs 14bis and 14bbis
pivot_table_14_2 = pivot_table_14.copy()
# Sort pivot_table_combined_2 by 'Pty Indice' - Update 08/28
#pivot_table_14_2 = pivot_table_14_2.sort_values(by='Pty Indice')
pivot_table_14_2.sort_values(by=['Priority', 'Pty Indice'], inplace=True)
# Mapping on pivot_table_combined to get program
pivot_table_14_2['Program'] = pivot_table_14_2['Pty Indice'].map(indice_to_program)
custom_palette14bis = {
"IDD Expected Total Sales": "rgba(68, 114, 196, 0.8)", # #4472C4 with alpha 0.8
"IDD Expected Total Margin": "rgba(63, 201, 89, 0.5)", # #3FC959 with alpha 0.5
"IDD Current Sales (Total)": "#4472C3",
"IDD Current Margin (Total)": "#548235",
}
custom_palette14bis_2 = {
"IDD Expected Total Sales": "rgba(68, 114, 196, 0.8)", # #4472C4 with alpha 0.8
"IDD Expected Total Margin": "rgba(63, 201, 89, 0.5)", # #3FC959 with alpha 0.5
"IDD Realized Sales": "#4472C3",
"IDD Realized Margin": "#548235",
}
custom_palette14bbis = {
"IDD Current Margin (%)": "#E2EFDA",
"% Completion": "#7FDB91",
"% DPAS Order": "#E4DFEC",
"IDD Expected ROI (Total)": "#568838",
}
#//////////////////////////////////////////////////
##################################################
# Create graph 14bis and 14bbis ---> Financial KPI
# --> To be update 09/23 to use df_Historic instead of df_Snapshot to calculate the 'Realized sales' and 'Realized Margin'
# The calculation should be based on the real data from the df_Historic trunover Report including the change of price over time
# New columns introduced in df_Snapshot:
# df_snapshot['IDD AVG realized sales price [USD]']
# df_snapshot['IDD AVG realized Margin Standard [USD]']
# df_snapshot['IDD AVG realized Margin [%]']
####################################################
##### New 09/24 to replace Graph 14bis with newlly added column in df_snapshot
def create_plot_14bis_2():
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
mask = pd.Series(True, index=pivot_table_14_2.index)
if selected_program != 'All':
mask &= (pivot_table_14_2['Program'] == selected_program)
if selected_priority != 'All':
mask &= (pivot_table_14_2['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (pivot_table_14_2['Pty Indice'] == selected_indice)
filtered_df = pivot_table_14_2.loc[mask].copy()
#####################################################################
# Dynamic y bounds for create_plot_14bis
#####################################################################
if filtered_df.empty:
return create_placeholder_plot('No data available for the selected filters.')
max_quantity_value14bis_2 = filtered_df[['IDD Expected Total Sales', 'IDD Expected Total Margin', 'IDD Realized Sales', 'IDD Realized Margin']].max().max()
min_quantity_value14bis_2 = filtered_df[['IDD Expected Total Sales', 'IDD Expected Total Margin', 'IDD Realized Sales', 'IDD Realized Margin']].min().min()
max_y_bound14bis_2 = max_quantity_value14bis_2 * 2
min_y_bound14bis_2 = min_quantity_value14bis_2 * 2 if min_quantity_value14bis_2 < 0 else 0
melted_df = filtered_df.melt(id_vars=['Pty Indice'],
value_vars=['IDD Expected Total Sales', 'IDD Expected Total Margin', 'IDD Realized Sales', 'IDD Realized Margin'],
var_name='Quantity Type', value_name='Quantity Value')
# Add a column with formatted values in thousands with '$Xk' prefix and rounded to whole numbers
#melted_df['Formatted Value'] = melted_df['Quantity Value'].apply(lambda x: f"${x / 1000:,.0f}k")
#############################################
# Set Order of melted_df --> Order of y label
#############################################
# Define the order of categories for 'Quantity Type'
unique_quantity_types = ['IDD Realized Margin', 'IDD Realized Sales', 'IDD Expected Total Margin', 'IDD Expected Total Sales']
# Convert 'Quantity Type' to a categorical type with the defined order
melted_df['Quantity Type'] = pd.Categorical(melted_df['Quantity Type'], categories=unique_quantity_types, ordered=True)
####################################################
# Define unique indices and calculate x_combined
##################################################
# New code 08/07
# Define constants
unique_indices = melted_df['Pty Indice'].astype('category').cat.categories
unique_quantity_types = melted_df['Quantity Type'].astype('category').cat.categories
num_types = len(unique_quantity_types) # Number of bars per 'Pty Indice'
num_indice = len(unique_indices) # Number of selected 'Pty Indice'
# Generate base_positions based on enumerate(unique_indices)
base_positions = {indice: i * (num_indice + 1) for i, indice in enumerate(unique_indices)}
#define gaps -- The gap is suppose to change based on the number of num_indice (for num_indice = 4 -- gap = 0.5 works well)
#gap = 0.5
gap = 1/(num_indice/2)
# Create a mapping of 'Pty Indice' to its index, starting from 0
indice_mapping = {indice: i for i, indice in enumerate(unique_indices)}
# Calculate x_combined for the bar positions
def calculate_x_combined(row):
pty_indice = row['Pty Indice']
quantity_type_code = melted_df['Quantity Type'].cat.codes[row.name]
# Get the index of the current 'Pty Indice'
indice = indice_mapping[pty_indice]
x_combined = base_positions[pty_indice] + quantity_type_code + 1/(num_indice + 1) + gap*indice #Calculation of the gap is yet to be refined as it does not work for all cases when Pty indice > 4
return x_combined
# Apply the function to calculate x_combined
melted_df['x_combined'] = melted_df.apply(calculate_x_combined, axis=1)
#####################################################
#New 09/11 - Calculate Y position
# Calculate the y_offset dynamically based on the 'Quantity Value'
#melted_df['y_position'] = melted_df['Quantity Value'] + melted_df['Quantity Value']*0.1
# Compute the maximum value of Quantity Value
max_quantity_value = melted_df['Quantity Value'].max()
# Calculate the 5% offset of the maximum value
offset = max_quantity_value * 0.05
# Define the function to calculate y_position with the conditional offset
def calculate_y_position(quantity_value):
if quantity_value >= 0:
return quantity_value + offset
else:
return offset # Apply offset in the opposite direction for negative values
# Apply the function to the DataFrame
melted_df['y_position'] = melted_df['Quantity Value'].apply(calculate_y_position)
#print('melted_df')
#display(melted_df)
plot = melted_df.hvplot.bar(
x='Pty Indice',
y='Quantity Value',
by='Quantity Type',
color='Quantity Type',
cmap=custom_palette14bis_2,
#title='IDD Total Sales & IDD Marge per Pty Indice by Top-Level Status, Production Status & Product Category',
xlabel='Pty Indice',
ylabel='[K$]',
legend='top_right',
stacked=False,
bar_width=0.6, # Set bar width - 09/12
#padding=1,
tools=[],
).opts(
xrotation=90,
)
updated_bokeh_plot = hv.render(plot, backend='bokeh')
updated_bokeh_plot.tools = [tool for tool in updated_bokeh_plot.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Pty Indice", "@Pty_Indice"),
("KPI", "@color"),
("Quantity Value", "@Quantity_Value{($0,0k)}") # Format values: thousands with 'K' # Quantity_Value with the '_' otherwise that does not work!
]
updated_bokeh_plot.add_tools(hover)
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
#updated_bokeh_plot.tools = [tool for tool in updated_bokeh_plot.tools if not isinstance(tool, WheelZoomTool)]
# 09/12 - Set wheel woom inactive
updated_bokeh_plot.toolbar.active_scroll = None
updated_bokeh_plot.xaxis.major_label_text_font_size = '0pt'
updated_bokeh_plot.yaxis.major_label_text_font_size = '10pt'
#updated_bokeh_plot.title.text_font_size = '8pt'
#updated_bokeh_plot.title.text_color = "#002570"
updated_bokeh_plot.xaxis.axis_line_width = 2
updated_bokeh_plot.yaxis.axis_line_width = 2
updated_bokeh_plot.xaxis.major_label_orientation = 'vertical'
updated_bokeh_plot.yaxis.major_label_orientation = 'horizontal'
updated_bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
updated_bokeh_plot.xgrid.grid_line_color = None
updated_bokeh_plot.ygrid.grid_line_color = '#F2F2F2'
updated_bokeh_plot.ygrid.grid_line_dash = [6, 4]
updated_bokeh_plot.y_range = Range1d(start=min_y_bound14bis_2, end=max_y_bound14bis_2)
updated_bokeh_plot.toolbar.logo = None
updated_bokeh_plot.legend.label_text_font_size = '8pt' # Set the font size of legend text
# Add custom formatted title
updated_bokeh_plot.add_layout(Title(
text="Financial KPI",
align='center',
text_font_size='10pt', # Adjust font size for the title
text_color="#002570" # Adjust color for the title
), 'above')
# Format the y-axis ticks in thousands with a dollar sign
updated_bokeh_plot.yaxis.formatter =CustomJSTickFormatter(code="""
return '$' + (tick / 1000).toFixed(0) + 'k';
""")
#Format the y-label to display on the graph
melted_df['formatted_labels'] = melted_df['Quantity Value'].apply(lambda x: f"${x / 1000:.0f}k")
# Add labels on top of the bars
source = ColumnDataSource(melted_df)
labels = LabelSet(
x= 'x_combined',
y='y_position',
#x= 'Quantity Type',
#y='Quantity Value',
#text='Quantity Value',
text='formatted_labels',
level='glyph',
source=source,
text_font_size='8pt',
text_font_style='bold', # Set the font style to bold
text_align='center',
#text_baseline='bottom', # Place labels above the bars #09/11
#y_offset=5, #09/11
text_color={'field': 'Quantity Type', 'transform': CategoricalColorMapper(
factors=unique_quantity_types, palette=[custom_palette14bis_2[qtype] for qtype in unique_quantity_types]
)}
)
updated_bokeh_plot.add_layout(labels)
return updated_bokeh_plot
#//////////////////////////////////////////////////
###################################################
# create_plot_14bbis
###################################################
def create_plot_14bbis():
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
mask = pd.Series(True, index=pivot_table_14_2.index)
if selected_program != 'All':
mask &= (pivot_table_14_2['Program'] == selected_program)
if selected_priority != 'All':
mask &= (pivot_table_14_2['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (pivot_table_14_2['Pty Indice'] == selected_indice)
filtered_df = pivot_table_14_2.loc[mask].copy()
#####################################################################
# Dynamic y bounds for create_plot_14bbis
#####################################################################
if filtered_df.empty:
return create_placeholder_plot('No data available for the selected filters.')
max_margin_value14bbis = filtered_df[['IDD Current Margin (%)', '% Completion', '% DPAS Order', 'IDD Expected ROI (Total)']].max().max()
min_margin_value14bbis = filtered_df[['IDD Current Margin (%)', '% Completion', '% DPAS Order', 'IDD Expected ROI (Total)']].min().min()
max_y_bound14bbis = max_margin_value14bbis * 2
# Add 10% to min value if any of the values are negative, otherwise set to 0
min_y_bound14bbis = min_margin_value14bbis * 2 if min_margin_value14bbis < 0 else 0
melted_df = filtered_df.melt(id_vars=['Pty Indice'],
value_vars=['IDD Current Margin (%)', '% Completion', '% DPAS Order', 'IDD Expected ROI (Total)'],
var_name='Quantity Type', value_name='Quantity Value')
#############################################
# Set Order of melted_df --> Order of y label
#############################################
# Define the order of categories for 'Quantity Type'
unique_quantity_types = ['IDD Expected ROI (Total)', '% DPAS Order', '% Completion', 'IDD Current Margin (%)']
# Convert 'Quantity Type' to a categorical type with the defined order
melted_df['Quantity Type'] = pd.Categorical(melted_df['Quantity Type'], categories=unique_quantity_types, ordered=True)
####################################################
# Define unique indices and calculate x_combined
##################################################
# New code 08/07
# Define constants
unique_indices = melted_df['Pty Indice'].astype('category').cat.categories
unique_quantity_types = melted_df['Quantity Type'].astype('category').cat.categories
num_types = len(unique_quantity_types) # Number of bars per 'Pty Indice'
num_indice = len(unique_indices) # Number of selected 'Pty Indice'
# Generate base_positions based on enumerate(unique_indices)
base_positions = {indice: i * (num_indice + 1) for i, indice in enumerate(unique_indices)}
#define gaps -- The gap is suppose to change based on the number of num_indice (for num_indice = 4 -- gap = 0.5 works well)
#gap = 0.5
gap = 1/(num_indice/2)
# Create a mapping of 'Pty Indice' to its index, starting from 0
indice_mapping = {indice: i for i, indice in enumerate(unique_indices)}
# Calculate x_combined for the bar positions
def calculate_x_combined(row):
pty_indice = row['Pty Indice']
quantity_type_code = melted_df['Quantity Type'].cat.codes[row.name]
# Get the index of the current 'Pty Indice'
indice = indice_mapping[pty_indice]
x_combined = base_positions[pty_indice] + quantity_type_code + 1/(num_indice + 1) + gap*indice #Calculation of the gap is yet to be refined as it does not work for all cases when Pty indice > 4
return x_combined
# Apply the function to calculate x_combined
melted_df['x_combined'] = melted_df.apply(calculate_x_combined, axis=1)
#####################################################
#New 09/11 - Calculate Y position
# Compute the maximum value of Quantity Value
max_quantity_value = melted_df['Quantity Value'].max()
# Calculate the 5% offset of the maximum value
offset = max_quantity_value * 0.05
# Define the function to calculate y_position with the conditional offset
def calculate_y_position(quantity_value):
if quantity_value >= 0:
return quantity_value + offset
else:
return offset # Apply offset in the opposite direction for negative values
# Apply the function to the DataFrame
melted_df['y_position'] = melted_df['Quantity Value'].apply(calculate_y_position)
plot = melted_df.hvplot.bar(
x='Pty Indice',
y='Quantity Value',
by='Quantity Type',
color='Quantity Type',
cmap=custom_palette14bbis,
#title='IDD % Margin per Pty Indice by Top-Level Status, Production Status & Product Category',
xlabel='Pty Indice',
ylabel='IDD % Margin',
legend='top_right',
stacked=False,
bar_width=0.6, # Set bar width - 09/12
#padding=0.1,
tools=[],
).opts(
xrotation=90,
)
updated_bokeh_plot = hv.render(plot, backend='bokeh')
updated_bokeh_plot.tools = [tool for tool in updated_bokeh_plot.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Pty Indice", "@Pty_Indice"),
("KPI", "@color"),
("Value", "@Quantity_Value%"), # 08/09
]
updated_bokeh_plot.add_tools(hover)
updated_bokeh_plot.xaxis.major_label_text_font_size = '0pt'
updated_bokeh_plot.yaxis.major_label_text_font_size = '10pt'
#updated_bokeh_plot.title.text_font_size = '8pt'
#updated_bokeh_plot.title.text_color = "#002570"
updated_bokeh_plot.xaxis.axis_line_width = 2
updated_bokeh_plot.yaxis.axis_line_width = 2
updated_bokeh_plot.xaxis.major_label_orientation = 'vertical'
updated_bokeh_plot.yaxis.major_label_orientation = 'horizontal'
updated_bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
updated_bokeh_plot.xgrid.grid_line_color = None
updated_bokeh_plot.ygrid.grid_line_color = '#F2F2F2'
updated_bokeh_plot.ygrid.grid_line_dash = [6, 4]
updated_bokeh_plot.y_range = Range1d(start=min_y_bound14bbis, end=max_y_bound14bbis)
updated_bokeh_plot.toolbar.logo = None
updated_bokeh_plot.legend.label_text_font_size = '8pt' # Set the font size of legend text
# Add custom formatted title
updated_bokeh_plot.add_layout(Title(
text="IDD % Margin, % Completion, % DPAS Order \n& Expected ROI per Pty Indice",
align='center',
text_font_size='10pt', # Adjust font size for the title
text_color="#002570" # Adjust color for the title
), 'above')
# Format y-axis ticks as percentages
updated_bokeh_plot.yaxis.formatter =CustomJSTickFormatter(code="""
return (tick).toFixed(0) + '%';
""")
# Format labels to include percentage sign
melted_df['formatted_labels'] = melted_df['Quantity Value'].apply(lambda x: f"{x:.0f}%")
# Add labels on top of the bars
source = ColumnDataSource(melted_df)
labels = LabelSet(
x= 'x_combined',
y = 'y_position', # Use y_position for the vertical position of the labels
#x= 'Quantity Type',
#y='Quantity Value',
#text='Quantity Value',
text='formatted_labels',
level='glyph',
source=source,
text_font_size='8pt',
text_font_style='bold', # Set the font style to bold
text_align='center',
#text_baseline='bottom', # Place labels above the bars
#text_baseline='text_baseline', # Use text_baseline for dynamic alignment #09/11 not working
#y_offset=5,
#y_offset='y_offset', #09/11 not working
text_color={'field': 'Quantity Type', 'transform': CategoricalColorMapper(
factors=unique_quantity_types, palette=[custom_palette14bbis[qtype] for qtype in unique_quantity_types]
)}
)
updated_bokeh_plot.add_layout(labels)
return updated_bokeh_plot
###############
# Inital call
###############
plot_pane_14bis_2 = pn.pane.Bokeh(create_plot_14bis_2())
plot_pane_14bbis = pn.pane.Bokeh(create_plot_14bbis())
##############################################################
# Update methods to include messages when no data is available
###############################################################
def update_plot_14bis_2(event):
plot_pane_14bis_2.object = create_plot_14bis_2()
def update_plot_14bbis(event):
plot_pane_14bbis.object = create_plot_14bbis()
#New 08/28
#######################################################################################
# Create plot_15bis base on pivot_table_15 with production_table_pane attacehd to it
#######################################################################################
# Custom color palette for the new plot
custom_palette15bis = {
"Standard Time (Routing, full ASSY)": "#6699FF", # Blue for Standard Time
"Actual Time (AVG Prod, full ASSY)": "#A2C075", # Green for actual time
"Standard Deviation (on Actual Time, full ASSY)": "#FF5733", # Orange for standard deviation
"Actual Time (AVG Prod, Top-Level only)": "#63BE7B", # bleu
}
#//////////////////////////////////////////////////
###################################################
# create_plot_15bis
###################################################
def create_plot_15bis():
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
mask = pd.Series(True, index=pivot_table_15.index)
if selected_program != 'All':
mask &= (pivot_table_15['Program'] == selected_program)
if selected_priority != 'All':
mask &= (pivot_table_15['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (pivot_table_15['Pty Indice'] == selected_indice)
filtered_df = pivot_table_15.loc[mask].copy()
if filtered_df.empty:
return create_placeholder_plot('No data available for the selected filters.')
#####################################################################
# Dynamic y bounds for create_plot_14bbis
#####################################################################
max_time_value15bis = filtered_df[['Standard Time (Routing, full ASSY)', 'Actual Time (AVG Prod, full ASSY)', 'Standard Deviation (on Actual Time, full ASSY)', 'Actual Time (AVG Prod, Top-Level only)']].max().max()
min_time_value15bis = filtered_df[['Standard Time (Routing, full ASSY)', 'Actual Time (AVG Prod, full ASSY)', 'Standard Deviation (on Actual Time, full ASSY)', 'Actual Time (AVG Prod, Top-Level only)']].min().min()
max_y_bound15bis = max_time_value15bis * 2
min_y_bound15bis = min_time_value15bis * 2 if min_time_value15bis < 0 else 0
# Melt the dataframe to reshape it for the plot
melted_df = filtered_df.melt(
id_vars=['Pty Indice'],
#value_vars=['Standard Time (Routing, full ASSY)', 'Actual Time (AVG Prod, full ASSY)', 'Standard Deviation (on Actual Time, full ASSY)', 'Actual Time (AVG Prod, Top-Level only)'],
value_vars=['Standard Time (Routing, full ASSY)', 'Actual Time (AVG Prod, full ASSY)', 'Standard Deviation (on Actual Time, full ASSY)'], # Without 'Actual Time (AVG Prod, Top-Level only)'
var_name='Time Type',
value_name='Time Value'
)
#############################################
# Set Order of melted_df --> Order of y label
#############################################
# Define the order for 'Time Type'
#unique_time_types = ['Actual Time (AVG Prod, Top-Level only)', 'Standard Deviation (on Actual Time, full ASSY)' , 'Actual Time (AVG Prod, full ASSY)', 'Standard Time (Routing, full ASSY)']
unique_time_types = ['Standard Deviation (on Actual Time, full ASSY)' , 'Actual Time (AVG Prod, full ASSY)', 'Standard Time (Routing, full ASSY)'] # Without 'Actual Time (AVG Prod, Top-Level only)'
melted_df['Time Type'] = pd.Categorical(melted_df['Time Type'], categories=unique_time_types, ordered=True)
####################################################
# Define unique indices and calculate x_combined
##################################################
# New code 08/07
# Define constants
unique_indices = melted_df['Pty Indice'].astype('category').cat.categories
unique_time_types = melted_df['Time Type'].astype('category').cat.categories
num_types = len(unique_time_types) # Number of bars per 'Pty Indice'
num_indice = len(unique_indices) # Number of selected 'Pty Indice'
# Generate base_positions based on enumerate(unique_indices)
base_positions = {indice: i * (num_indice + 1) for i, indice in enumerate(unique_indices)}
#define gaps -- The gap is suppose to change based on the number of num_indice (for num_indice = 4 -- gap = 0.5 works well)
#gap = 0.5
gap = 1/(num_indice/2)
# Create a mapping of 'Pty Indice' to its index, starting from 0
indice_mapping = {indice: i for i, indice in enumerate(unique_indices)}
# Calculate x_combined for the bar positions
def calculate_x_combined(row):
pty_indice = row['Pty Indice']
time_type_code = melted_df['Time Type'].cat.codes[row.name]
# Get the index of the current 'Pty Indice'
indice = indice_mapping[pty_indice]
x_combined = base_positions[pty_indice] + time_type_code + 1/(num_indice + 1) + gap*indice #Calculation of the gap is yet to be refined as it does not work for all cases when Pty indice > 4
return x_combined
# Apply the function to calculate x_combined
melted_df['x_combined'] = melted_df.apply(calculate_x_combined, axis=1)
#####################################################
# Create the plot for Graph 15bis
plot_15bis = melted_df.hvplot.bar(
x='Pty Indice',
y='Time Value',
by='Time Type',
color='Time Type',
#title='Standard Time VS Actual Time',
xlabel='Pty Indice',
ylabel='Time [hours]',
cmap=custom_palette15bis,
legend='top_right',
stacked=False,
bar_width=0.6, # Set bar width - 09/12
#padding=1,
tools=[],
).opts(xrotation=90)
# Customize the Bokeh plot
bokeh_plot_15bis = hv.render(plot_15bis, backend='bokeh')
bokeh_plot_15bis.tools = [tool for tool in bokeh_plot_15bis.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Pty Indice", "@Pty_Indice"),
("Time Type", "@color"),
("Time Value", "@Time_Value{0.0} hours"),
]
bokeh_plot_15bis.add_tools(hover)
# 09/12 - Set wheel woom inactive
bokeh_plot_15bis.toolbar.active_scroll = None
# Further customizations
bokeh_plot_15bis.xaxis.major_label_text_font_size = '0pt'
bokeh_plot_15bis.yaxis.major_label_text_font_size = '10pt'
bokeh_plot_15bis.title.text_font_size = '10pt'
bokeh_plot_15bis.title.text_color = "#002570"
bokeh_plot_15bis.xaxis.axis_line_width = 2
bokeh_plot_15bis.yaxis.axis_line_width = 2
bokeh_plot_15bis.xaxis.major_label_orientation = 'vertical'
bokeh_plot_15bis.yaxis.major_label_orientation = 'horizontal'
bokeh_plot_15bis.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot_15bis.xgrid.grid_line_color = None
bokeh_plot_15bis.ygrid.grid_line_color = '#F2F2F2'
bokeh_plot_15bis.ygrid.grid_line_dash = [6, 4]
bokeh_plot_15bis.toolbar.logo = None
bokeh_plot_15bis.y_range = Range1d(start=min_y_bound15bis, end=max_y_bound15bis)
bokeh_plot_15bis.legend.label_text_font_size = '8pt'
# Add custom formatted title
bokeh_plot_15bis.add_layout(Title(
text="Production KPI",
align='center',
text_font_size='10pt',
text_color="#002570"
), 'above')
# Add labels on top of the bars
source = ColumnDataSource(melted_df)
labels = LabelSet(
x='x_combined',
y='Time Value',
text='Time Value',
level='glyph',
source=source,
text_font_size='8pt',
text_font_style='bold', # Set the font style to bold
text_align='center',
text_baseline='bottom',
y_offset=5,
text_color={'field': 'Time Type', 'transform': CategoricalColorMapper(
factors=unique_time_types, palette=[custom_palette15bis[ttype] for ttype in unique_time_types]
)}
)
bokeh_plot_15bis.add_layout(labels)
return bokeh_plot_15bis
# Initial call
plot_pane_15bis = pn.pane.Bokeh(create_plot_15bis())
# Update method
def update_plot_15bis(event):
plot_pane_15bis.object = create_plot_15bis()
#///////////////////////////////////////
########################################
# Create table related to Graph-15
########################################
#///////////////////////////////////////
# Update 09/10 WIP --> Include 'Total Top-Level Qty' and 'Total Components Qty' in he table 'Top-Level WO Count'*'Qty per WO' and 'Total WO Count'*'Total Components Qty'
# Table containing 'Pty Indice', 'Total WO Count' and 'Top-Level WO Count' based on the widget (Program, Priority, Pty Indice) selection of |Products Status|
pivot_table_15_2 = pivot_table_15.copy()
def create_production_table_by_pty_indice(df):
return df[['Pty Indice', 'Total WO Count', 'Top-Level WO Count', 'Total Top-Level Qty', 'Total sub-Level Qty']]
# Define the function to update the production table based on widget values
def update_production_table_by_pty_indice(event=None):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
mask = pd.Series(True, index=pivot_table_15_2.index)
if selected_program != 'All':
mask &= (pivot_table_15_2['Program'] == selected_program)
if selected_priority != 'All':
mask &= (pivot_table_15_2['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (pivot_table_15_2['Pty Indice'] == selected_indice)
filtered_df = pivot_table_15_2.loc[mask].copy()
if filtered_df.empty:
updated_table = create_placeholder_plot('No data available for the selected filters.')
else:
updated_table = create_production_table_by_pty_indice(filtered_df)
# Update the production table pane
production_table_by_pty_indice_pane.object = updated_table
# Initialize the production table with default values
initial_production_table_by_pty_indice = create_production_table_by_pty_indice(pivot_table_15_2)
production_table_by_pty_indice_pane = pn.pane.DataFrame(initial_production_table_by_pty_indice, width=420, index=False) #09/10
# Initial table setup
update_production_table_by_pty_indice()
# Attach the update function to widget value changes
program_widget.param.watch(update_production_table_by_pty_indice, 'value')
priority_widget.param.watch(update_production_table_by_pty_indice, 'value')
indice_widget.param.watch(update_production_table_by_pty_indice, 'value')
##################################################################################################################
# 09/25 - Create a second panda datafram related to Graph 15 with data from df_Snapshot related to Production KPI
##################################################################################################################
# The table should contain 'Pty Indice', 'Actual vs Standard time [%]', 'Deviation vs Actual [%]'
# Apply color formating on 'Deviation vs Actual [%]' : green if < 30%, orange if 30 to 50% and red if > 50%
# Step 1: Create a copy of df_Snapshot and rename the columns
df_Snapshot_prod_KPI = df_Snapshot.copy()
df_Snapshot_prod_KPI.rename(columns={
'Actual vs Standard time [%]': 'Standard time to Actual time [%]',
}, inplace=True)
# Need to keep 'Priority', 'Program' for the widget to work
relevant_columns_KPI = ['Pty Indice', 'Priority', 'Program',
'Standard time to Actual time [%]', 'Deviation vs Actual [%]']
df_Snapshot_prod_KPI = df_Snapshot_prod_KPI[relevant_columns_KPI]
# Convert percentage strings to float in numeric DataFrame
def convert_percentage_columns(df, percentage_columns):
for col in percentage_columns:
df[col] = (
df[col]
.str.replace('%', '', regex=False)
.astype(float) / 100
)
return df
# Step 3: Create a numeric DataFrame for percentage calculations
df_Snapshot_KPI_numeric = df_Snapshot_prod_KPI.copy()
percentage_columns_KPI = ['Standard time to Actual time [%]', 'Deviation vs Actual [%]']
df_Snapshot_KPI_numeric = convert_percentage_columns(df_Snapshot_KPI_numeric, percentage_columns_KPI)
# Step 4: Function to create a color dictionary
def create_color_dictionary(df, column):
color_dict = {}
for value in df[column]:
try:
# Handle both string and float types
if isinstance(value, str):
num_value = float(value.replace('%', '')) / 100
else:
num_value = value # Assume it's already a float
# Determine color based on value ranges
if num_value < 0.3:
color_dict[num_value] = 'green'
elif 0.3 <= num_value < 0.5:
color_dict[num_value] = 'orange'
else:
color_dict[num_value] = 'red'
except ValueError:
color_dict[value] = 'black' # Handle non-convertible values
return color_dict
# Create a color dictionary for 'Deviation vs Actual [%]'
color_mapping = create_color_dictionary(df_Snapshot_prod_KPI, 'Deviation vs Actual [%]')
# Function to apply color formatting to the DataFrame
def apply_color_formatting_prod_KPI(df, color_dict, column):
"""Apply conditional color formatting to a specified column in the DataFrame."""
def color_deviation(val):
"""Return the corresponding color based on the value."""
try:
# Handle both string and float types
if isinstance(val, str):
num_value = float(val.replace('%', '')) / 100
else:
num_value = val # Assume it's already a float
color = color_dict.get(num_value, 'black') # Default to black if not found
except ValueError:
color = 'black' # Handle conversion failure
return f'color: {color}'
# Create a styled DataFrame with color formatting
styled_df = df.style.applymap(color_deviation, subset=[column])
# Center text in both header and cells
styled_df.set_table_styles(
[
{'selector': 'th', 'props': [('text-align', 'center')]}, # Center headers
{'selector': 'td', 'props': [('text-align', 'center')]} # Center cells
]
)
return styled_df
# Prepare the display DataFrame
df_display = df_Snapshot_prod_KPI.copy()
# Apply the conditional color formatting to the 'Deviation vs Actual [%]' column
styled_df = apply_color_formatting_prod_KPI(df_display, color_mapping, 'Deviation vs Actual [%]')
# Update the KPI table function
def update_kpi_table_prod(event):
"""Update the KPI table based on selected filters."""
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Create a mask for filtering
mask = pd.Series(True, index=df_Snapshot_prod_KPI.index)
if selected_program != 'All':
mask &= (df_Snapshot_prod_KPI['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_Snapshot_prod_KPI['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_Snapshot_prod_KPI['Pty Indice'] == selected_indice)
# Filter the DataFrame
filtered_df = df_Snapshot_prod_KPI.loc[mask].copy().reset_index(drop=True)
# Drop 'Priority' and 'Program' from the filtered DataFrame for display
filtered_df_display = filtered_df.drop(columns=['Priority', 'Program'])
# Apply the conditional color formatting to the 'Deviation vs Actual [%]' column
filtered_styled_df = apply_color_formatting_prod_KPI(filtered_df_display, color_mapping, 'Deviation vs Actual [%]').hide(axis='index')
# Update the KPI table pane
kpi_table_pane_prod.object = filtered_styled_df
# Create the initial styled DataFrame without index
def create_styled_dataframe(df):
"""Create a styled DataFrame without the index."""
return apply_color_formatting_prod_KPI(df, color_mapping, 'Deviation vs Actual [%]').hide(axis='index')
# Initialize the KPI table pane
kpi_table_pane_prod = pn.pane.DataFrame(
create_styled_dataframe(df_Snapshot_prod_KPI.copy().reset_index(drop=True)), # Create a styled DataFrame
width=420,
)
# Initialize update_kpi_table_prod
update_kpi_table_prod(None)
# Attach update function to widget changes
program_widget.param.watch(update_kpi_table_prod, 'value')
priority_widget.param.watch(update_kpi_table_prod, 'value')
indice_widget.param.watch(update_kpi_table_prod, 'value')
#####################################################################################
# 09/24 - Create a panda datafram to summazize the production KPI from df_Snapashot
#####################################################################################
# First rename 'Critical Qty' to 'Critical Qty Initial' and 'IDD Production Cost (unit)' to 'IDD current cost (unit)'
# The table_production_KPI contain: 'Pty Indice', 'IDD Marge Standard (unit)', 'IDD Sale Price', 'IDD Current Margin (%)', 'IDD current cost (unit)', 'IDD AVG realized Margin [%]', 'IDD Corrected Margin [%]', 'Critical Qty Initial'
# Color in darf green font when 'IDD AVG realized Margin [%]', 'IDD Current Margin (%)', 'IDD Corrected Margin [%]' are positive and in red font when negative
# Make sure that the following column are currency (USD): 'IDD Marge Standard (unit)', 'IDD Sale Price', 'IDD current cost (unit)',
# Make sure that the following column are percentage (%): 'IDD AVG realized Margin [%]', 'IDD Current Margin (%)', 'IDD Corrected Margin [%]'
# Step 1: Create a copy of df_Snapshot and rename the columns
df_Snapshot_prod = df_Snapshot.copy()
df_Snapshot_prod.rename(columns={
'Critical Qty': 'Critical Qty (Initial)',
'IDD Production Cost (unit)': 'IDD current cost (per unit)',
'IDD Marge Standard (unit)': 'IDD Margin Standard (per unit)',
'IDD Current Margin (%)': 'IDD Current Margin [%]'
}, inplace=True)
# Step 2: Keep only the necessary columns
relevant_columns = ['Pty Indice', 'Priority', 'Program',
'IDD Margin Standard (per unit)', 'IDD Sale Price',
'IDD Current Margin [%]', 'IDD current cost (per unit)',
'IDD AVG realized Margin [%]', 'IDD Corrected Margin [%]',
'Critical Qty (Initial)']
df_Snapshot_prod = df_Snapshot_prod[relevant_columns]
# Step 3: Create a numeric DataFrame for percentage calculations
df_Snapshot_numeric = df_Snapshot_prod.copy()
percentage_columns = ['IDD Current Margin [%]', 'IDD AVG realized Margin [%]', 'IDD Corrected Margin [%]']
# Convert percentage strings to float in numeric DataFrame
def convert_percentage_columns(df, percentage_columns):
for col in percentage_columns:
df[col] = (
df[col]
.str.replace('%', '', regex=False)
.astype(float) / 100
)
return df
df_Snapshot_numeric = convert_percentage_columns(df_Snapshot_numeric, percentage_columns)
# --> WIP 10/07 <--
# Step 4: Create a color mapping for percentage columns
def create_color_mapping_percentage(df, columns):
"""Create a color mapping for multiple percentage columns based on value ranges."""
color_dict = {}
for column in columns:
for idx, value in df[column].items():
try:
num_value = float(value) # Assume value is already a float
# Define the color based on value ranges
if num_value > 0:
color_dict[(idx, column)] = 'green'
elif num_value < 0:
color_dict[(idx, column)] = 'red'
else:
color_dict[(idx, column)] = 'black'
except ValueError:
color_dict[(idx, column)] = 'black' # Fallback for non-convertible values
return color_dict
# Create color mappings for the relevant percentage columns
percentage_columns = ['IDD Current Margin [%]', 'IDD AVG realized Margin [%]', 'IDD Corrected Margin [%]']
color_mapping_margin = create_color_mapping_percentage(df_Snapshot_numeric, percentage_columns)
# Step 5: Function to apply color formatting
def apply_color_formatting_margin(df, color_dict):
"""Apply conditional color formatting to the percentage columns in the DataFrame."""
def color_deviation(val, idx, col):
"""Return the corresponding color based on the value."""
try:
# Get color for the (index, column) pair
color = color_dict.get((idx, col), 'black')
except ValueError:
color = 'black' # Handle conversion failure
return f'color: {color}'
# Create a styled DataFrame with color formatting for the relevant percentage columns
styled_df = df.style.apply(
lambda x: [color_deviation(x[col], x.name, col) for col in df.columns], axis=1
)
# Center text in both header and cells
styled_df.set_table_styles(
[
{'selector': 'th', 'props': [('text-align', 'center')]}, # Center headers
{'selector': 'td', 'props': [('text-align', 'center')]} # Center cells
]
)
return styled_df
# Step 6: Function to format currency values
def format_currency(value):
"""Format a number as currency with one decimal place."""
return f"${value:,.1f}"
# Update the KPI table function
def update_kpi_table(event):
"""Update the KPI table based on selected filters."""
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Create a mask for filtering
mask = pd.Series(True, index=df_Snapshot_prod.index)
if selected_program != 'All':
mask &= (df_Snapshot_prod['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_Snapshot_prod['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_Snapshot_prod['Pty Indice'] == selected_indice)
filtered_df = df_Snapshot_prod.loc[mask].copy().reset_index(drop=True)
filtered_numeric_df = df_Snapshot_numeric.loc[mask].copy().reset_index(drop=True)
# Apply currency formatting to the specified columns
currency_columns = ['IDD Margin Standard (per unit)', 'IDD Sale Price', 'IDD current cost (per unit)']
for col in currency_columns:
filtered_df[col] = filtered_df[col].apply(format_currency)
# Create a new color mapping for the filtered DataFrame
color_mapping_margin = create_color_mapping_percentage(filtered_numeric_df, percentage_columns)
# Apply the conditional color formatting to the relevant percentage columns
filtered_styled_df = apply_color_formatting_margin(filtered_df, color_mapping_margin).hide(axis='index')
# Update the KPI table pane
kpi_table_pane.object = filtered_styled_df
# Step 7: Create the initial styled DataFrame
def create_styled_dataframe(df):
"""Create a styled DataFrame without the index."""
return apply_color_formatting_margin(df, color_mapping_margin).hide(axis='index')
# Initialize the KPI table pane
kpi_table_pane = pn.pane.DataFrame(
create_styled_dataframe(df_Snapshot_prod.copy().reset_index(drop=True)), # Create a styled DataFrame
width=1250,
)
# Initialize and update KPI table
update_kpi_table(None)
# Attach update function to widget changes
program_widget.param.watch(update_kpi_table, 'value')
priority_widget.param.watch(update_kpi_table, 'value')
indice_widget.param.watch(update_kpi_table, 'value')
#####################################################################################
# 09/26 - Create a panda datafram to summazize the backlog KPI from df_Snapashot
#####################################################################################
# Build a table with 5 columns: pivot_table_14['Pty Indice'], , pivot_table_14['Priority'], pivot_table_14['Program'], pivot_table_14['% Completion'], pivot_table_14['% DPAS Order']
# Display only 'Pty Indice', % Completion' and '% DPAS Order'
# Function to format numeric values as percentages with one decimal point
def format_percentage(value):
"""Format a numeric value as a percentage with 1 decimal point."""
return "{:.1f}%".format(value) # No multiplication, directly format the value
def create_backlog_table():
"""Build a backlog table from a copy of pivot_table_14."""
# Create a copy of pivot_table_14 to avoid modifying the original DataFrame
backlog_table = pivot_table_14.copy()
# Select the relevant columns
backlog_table = backlog_table[['Pty Indice', 'Priority', 'Program', '% Completion', '% Completion Total Backlog', '% DPAS Order']]
# Rename '% Completion' to '% Completion Critical Qty'
backlog_table.rename(columns={'% Completion': '% Completion Critical Qty'}, inplace=True)
# Return the backlog table without additional formatting for calculations
return backlog_table
# Initialize the backlog_table globally to be accessible elsewhere
backlog_table = create_backlog_table()
def update_filtered_backlog_table(event):
"""Update the backlog table based on widget filters."""
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Re-create a copy of the backlog table for filtering
backlog_table = create_backlog_table()
# Apply filters based on the widget values
mask = pd.Series(True, index=backlog_table.index)
if selected_program != 'All':
mask &= (backlog_table['Program'] == selected_program)
if selected_priority != 'All':
mask &= (backlog_table['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (backlog_table['Pty Indice'] == selected_indice)
# Filter the backlog table based on the mask
filtered_backlog_table = backlog_table.loc[mask].reset_index(drop=True) # Reset index here
# Display only the columns: 'Pty Indice', '% Completion', '% DPAS Order'
display_columns = ['Pty Indice', '% Completion Total Backlog', '% Completion Critical Qty', '% DPAS Order']
filtered_display_table = filtered_backlog_table[display_columns].copy()
# Format the percentage columns for display with one decimal point
filtered_display_table['% Completion Critical Qty'] = filtered_display_table['% Completion Critical Qty'].apply(format_percentage)
filtered_display_table['% Completion Total Backlog'] = filtered_display_table['% Completion Total Backlog'].apply(format_percentage)
filtered_display_table['% DPAS Order'] = filtered_display_table['% DPAS Order'].apply(format_percentage)
# Create and style the DataFrame directly
styled_filtered_table = create_styled_dataframe(filtered_display_table)
# Update the Panel DataFrame pane
backlog_table_pane.object = styled_filtered_table
def create_styled_dataframe(df):
"""Create a styled DataFrame without the index and center values."""
# Create a styled DataFrame
styled_df = df.style.hide(axis='index') # Hide the index
# Set table styles for centering text in both headers and data cells
styled_df.set_table_styles(
[
{
'selector': 'th, td', # Select both headers and data cells
'props': [('text-align', 'center')] # Center text
}
],
axis=0 # Applies the styles to all columns
)
return styled_df
# Initialize the backlog_table_pane with the styled backlog table
backlog_table_pane = pn.pane.DataFrame(
create_styled_dataframe(backlog_table.reset_index(drop=True)), # Create a styled DataFrame
width=750,
)
# Trigger the first update of the pane
update_filtered_backlog_table(None)
# Attach the update function to widget changes to update the backlog table automatically
program_widget.param.watch(update_filtered_backlog_table, 'value')
priority_widget.param.watch(update_filtered_backlog_table, 'value')
indice_widget.param.watch(update_filtered_backlog_table, 'value')
#################################################################################
# New 09/26 - Text boxes related to production KPI - Production KPI tables
###################################################################################
# Styles for the header and the card
header_styles = {
"background_color": "#9EC5E4", # Header color
"font_color": "white" # Text color for header
}
# Custom CSS for the card and header
custom_css = f"""
<style>
.custom-card {{
border: 2px solid #A9A9A9; /* Gray border */
border-radius: 10px; /* Rounded corners */
background-color: #ECF4FA; /* Light background inside the card */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1); /* Subtle shadow */
padding: 15px; /* Padding inside the card */
}}
.custom-card-header {{
background-color: {header_styles["background_color"]}; /* Header background */
color: {header_styles["font_color"]}; /* Header text color */
padding: 10px;
font-size: 18px;
text-align: center;
font-weight: bold;
border-top-left-radius: 10px; /* Rounded top corners */
border-top-right-radius: 10px; /* Rounded top corners */
}}
</style>
"""
# Headers text for the card
header_html_Prod = f"<div class='custom-card-header'>Production KPI overview</div>"
header_html_Finance = f"<div class='custom-card-header'> Overview of Financial KPIs and the influence of Production KPIs</div>"
header_html_Backlog = f"<div class='custom-card-header'>Backlog KPI overview</div>"
#/////////////////////////////////////////////////////////////////////////////////
######################################################
# Create text box 'textbox_production_table_by_pty_indice_pane' under the table production_table_by_pty_indice_pane
######################################################
# 'Qty Shipped' - 'Total Top-Level Qty' Top-Level are filtered-out from the calculation due to related WOs considerede abberant value.
# 'Total Top-Level Qty' Top-Level are thus considered for the calcualtion.
# The calculation is made based on 'Total WO Count' data-point representinf the number of WOs, in which 'Top-Level WO Count' represents Top-Level WOs.
#/////////////////////////////////////////////////////////////////////////////////
# Initialize global variables
top_filtered_out_qty = 0
total_top_level_qty = 0
top_wo_count = 0
total_wo_count = 0
# Function to update the 'Top-Level Qty filtered-out' quantity
def update_top_filtered_out_qty(filtered_df_snapshot, filtered_df):
global top_filtered_out_qty
top_filtered_out_qty = filtered_df_snapshot['Shipped'].sum() - filtered_df['Total Top-Level Qty'].sum()
# Function to update the 'Total Top-Level Qty' based on filtered data
def update_total_top_level_qty(filtered_df):
global total_top_level_qty
total_top_level_qty = filtered_df['Total Top-Level Qty'].sum() if 'Total Top-Level Qty' in filtered_df else 0
# Function to update the 'Total WO Count' based on filtered data
def update_total_wo_count(filtered_df):
global total_wo_count
total_wo_count = filtered_df['Total WO Count'].sum() if 'Total WO Count' in filtered_df else 0
# Function to update the 'Top-Level WO Count' based on filtered data
def update_top_wo_count(filtered_df):
global top_wo_count
top_wo_count = filtered_df['Top-Level WO Count'].sum() if 'Top-Level WO Count' in filtered_df else 0
# Initialize the textbox to display results
textbox_production_table_by_pty_indice_pane = pn.pane.Markdown("", sizing_mode='stretch_width')
# Function to update quantities and text box based on widget selections
def update_textbox(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Initialize a boolean mask with all True values for pivot_table_15_2
mask = pd.Series(True, index=pivot_table_15_2.index)
# Apply filters based on selections for pivot_table_15_2
if selected_program != 'All':
mask &= (pivot_table_15_2['Program'] == selected_program)
if selected_priority != 'All':
mask &= (pivot_table_15_2['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (pivot_table_15_2['Pty Indice'] == selected_indice)
# Apply the mask to filter pivot_table_15_2
filtered_df = pivot_table_15_2.loc[mask].copy()
# Initialize a boolean mask for df_Snapshot
mask_snapshot = pd.Series(True, index=df_Snapshot.index)
# Apply filters based on selections for df_Snapshot
if selected_program != 'All':
mask_snapshot &= (df_Snapshot['Program'] == selected_program)
if selected_priority != 'All':
mask_snapshot &= (df_Snapshot['Priority'] == selected_priority)
if selected_indice != 'All':
mask_snapshot &= (df_Snapshot['Pty Indice'] == selected_indice)
# Filter df_Snapshot using the constructed mask
filtered_df_snapshot = df_Snapshot[mask_snapshot].copy()
# Update quantities based on the filtered DataFrames
update_top_filtered_out_qty(filtered_df_snapshot, filtered_df)
update_total_top_level_qty(filtered_df)
update_top_wo_count(filtered_df)
update_total_wo_count(filtered_df)
# Update the text box content with the actual values
textbox_production_table_by_pty_indice_pane.object = f"""
▷ <b>{top_filtered_out_qty}</b> Top-Level are filtered-out from the calculation. <br>
▷ <b>{total_top_level_qty}</b> Top-Level are considered for the calculation. <br>
▷ <b>{total_wo_count}</b> represents the number of WOs included in the calculation, with <b>{top_wo_count}</b> representing Top-Level WOs.
"""
# Attach the update function to widget value changes
program_widget.param.watch(update_textbox, 'value')
priority_widget.param.watch(update_textbox, 'value')
indice_widget.param.watch(update_textbox, 'value')
# Initial text box setup
update_textbox(None) # Call once to initialize with default values
#/////////////////////////////////////////////////////////////////////////////////
##################################################################################
# Create a text box 'textbox_kpi_table_prod' under the table kpi_table_prod
##################################################################################
# Standard deviation represents 'Deviation vs Actual [%]' of the Standard Time.
# Standard Time has to be incresed by 'Standard time to Actual time [%]' in order to reflect the Actual Time.
#/////////////////////////////////////////////////////////////////////////////////
# 'Deviation vs Actual [%]' and 'Standard time to Actual time [%]' are coming from
# Initialize global variables for the dynamic text
deviation_vs_actual = 0
standard_time_to_actual = 0
def convert_percentage_to_numeric(percentage):
"""Convert a percentage string to a numeric value (decimal)."""
if isinstance(percentage, str):
# Handle string input, stripping '%' and converting to decimal
return float(percentage.strip('%')) / 100
elif isinstance(percentage, (float, int)):
# Handle float or int input, already numeric
return percentage
else:
# Handle unexpected types, returning NaN or a default value
return float('nan') # Or return 0.0, depending on your requirement
# Function to update the dynamic values
def update_dynamic_values(filtered_df):
global deviation_vs_actual, standard_time_to_actual
# Convert columns to numeric values
if 'Deviation vs Actual [%]' in filtered_df:
filtered_df['Deviation vs Actual [%]'] = filtered_df['Deviation vs Actual [%]'].apply(convert_percentage_to_numeric)
deviation_vs_actual = filtered_df['Deviation vs Actual [%]'].mean() # Calculate mean
if 'Standard time to Actual time [%]' in filtered_df:
filtered_df['Standard time to Actual time [%]'] = filtered_df['Standard time to Actual time [%]'].apply(convert_percentage_to_numeric)
standard_time_to_actual = filtered_df['Standard time to Actual time [%]'].mean() # Calculate mean
# Create the dynamic text box for the KPI table
def update_textbox_kpi_table_prod(event):
selected_program = program_widget.value
selected_priority = priority_widget.value
selected_indice = indice_widget.value
# Create a mask for filtering df_Snapshot_prod_KPI
mask = pd.Series(True, index=df_Snapshot_prod_KPI.index)
if selected_program != 'All':
mask &= (df_Snapshot_prod_KPI['Program'] == selected_program)
if selected_priority != 'All':
mask &= (df_Snapshot_prod_KPI['Priority'] == selected_priority)
if selected_indice != 'All':
mask &= (df_Snapshot_prod_KPI['Pty Indice'] == selected_indice)
# Filter the DataFrame
filtered_df = df_Snapshot_prod_KPI.loc[mask].copy()
# Update dynamic values based on filtered DataFrame
update_dynamic_values(filtered_df)
# Update the text box content with the actual values
textbox_kpi_table_prod.object = f"""
▷ <b>Standard deviation</b> represents <b>{deviation_vs_actual:.0%}</b> of the Standard Time.
▷ <b>Standard Time</b> has to be increased by <b>{standard_time_to_actual:.0%}</b> to reflect the <b>Actual Time</b>.
"""
# Initialize the textbox to display results
textbox_kpi_table_prod = pn.pane.Markdown("", width=425)
# Attach the update function to widget value changes
program_widget.param.watch(update_textbox_kpi_table_prod, 'value')
priority_widget.param.watch(update_textbox_kpi_table_prod, 'value')
indice_widget.param.watch(update_textbox_kpi_table_prod, 'value')
# Initial text box setup
update_textbox_kpi_table_prod(None) # Call once to initialize with default values
#/////////////////////////////////////////////////////////////////////////////////
###################################################
# Create a text box 'textbox_kpi_table_finance' under the table 'kpi_table_pane'
####################################################
textbox_kpi_table_finance = pn.pane.Markdown(
"""
▷ <b>IDD Current Margin [%]</b>: Margin based on the current Standard Time and Sale Price <br>
▷ <b>IDD AVG realized Margin [%]</b>: Average Margin over time considering any potential change of price <br>
▷ <b>IDD Corrected Margin [%]</b>: Real Margin reflecting the potential difference between Standard Time and Actual Time. Calculated based on the average 2024 labor cost ($79.58/h). <br>
➥ The Corrected Margin is not exact, as it includes only the labor efficiency but does not account for other factors such as labor cost variance and material purchasing variances. However, it is more representative of the real margin than the IDD Current Margin, which does not consider the labor efficiency (Actual Time vs. Stanadrd Time).
""",
width=1270
)
#/////////////////////////////////////////////////////////////////////////////////
###################################################
# Create a text box 'textbox_kpi_table_finance' under the table 'kpi_table_pane'
####################################################
textbox_kpi_table_backlog = pn.pane.Markdown(
"""
▷ <b>% Completion Critical Qty</b>: Based on the critical quantity. It can be > 100%. <br>
▷ <b>% Completion Total</b>: Including follow-up orders.
""",
width=600
)
########################################################################################
#### Create the layout for the production card, including the header and other elements
########################################################################################
def create_kpi_summary_card():
# This includes the custom CSS and the header HTML
card_layout = pn.Column(
pn.pane.HTML(custom_css + header_html_Prod), # Apply custom styles and header
pn.Spacer(height=10), # Spacer for layout
pn.Row(
pn.Spacer(width=20),
pn.Column(
production_table_by_pty_indice_pane, # First KPI Table
pn.Spacer(height=5), # Space between elements
textbox_production_table_by_pty_indice_pane, # First text box
pn.Spacer(height=5), # Space between elements
kpi_table_pane_prod, # Second KPI Table
pn.Spacer(height=5), # Space between elements
textbox_kpi_table_prod, # Second text box
width=425,
),
),
css_classes=["custom-card"] # Apply the custom card styling here
)
return card_layout
# Create and display the KPI summary card
kpi_summary_card = create_kpi_summary_card()
########################################################################################
#### Create the layout for the Financial card, including the header and other elements
########################################################################################
def create_kpi_summary_card():
# This includes the custom CSS and the header HTML
card_layout = pn.Column(
pn.pane.HTML(custom_css + header_html_Finance), # Apply custom styles and header
pn.Spacer(height=10), # Spacer for layout
pn.Row(
pn.Spacer(width=20),
pn.Column(
kpi_table_pane, # Fianance Table
pn.Spacer(height=5), # Space between elements
textbox_kpi_table_finance, # First text box
),
),
css_classes=["custom-card"] # Apply the custom card styling here
)
return card_layout
# Create and display the KPI summary card
kpi_summary_card_finance = create_kpi_summary_card()
########################################################################################
#### Create the layout for the backlog card, including the header and other elements
########################################################################################
def create_kpi_summary_card_backlog():
# This includes the custom CSS and the header HTML
card_layout = pn.Column(
pn.pane.HTML(custom_css + header_html_Backlog), # Apply custom styles and header
pn.Spacer(height=10), # Spacer for layout
pn.Row(
pn.Spacer(width=20),
pn.Spacer(height=5),
pn.Column(
backlog_table_pane, # Backlog Table
pn.Spacer(height=5), # Space between elements
textbox_kpi_table_backlog, # First text box
),
),
css_classes=["custom-card"] # Apply the custom card styling here
)
return card_layout
# Create and display the KPI summary card
kpi_summary_card_backlog = create_kpi_summary_card_backlog()
###################################################################################
#//////////////////////////////////////////////////
########################################
# Create Graph13-13b-14-14b Dashboard
########################################
#//////////////////////////////////////////////////
# Set explicit width and height for each plot # 02/26
plot_pane_13bis.width = 370
plot_pane_13bis.height = 450
plot_pane_13bbis.width = 370
plot_pane_13bbis.height = 450
plot_pane_14bis_2.width = 370
plot_pane_14bis_2.height = 450
plot_pane_15bis.width = 370
plot_pane_15bis.height = 450
# Create vertical divier (gray vertical line for separation)
vertical_divider_med1 = pn.pane.HTML(
'<div style="width: 3px; height: 450px; background-color:#D9D9D9;"></div>',
)
vertical_divider_long = pn.pane.HTML(
'<div style="width: 3px; height: 770px; background-color:#D9D9D9;"></div>',
)
# Updated 09/26 - to integrate kpi_summary_card_backlog
# Define the layout for the plots and tables
combined_plots_layout = pn.Column(
pn.Row( # Main row to contain left and right columns with a vertical divider
pn.Column( # Left column for backlog graphs and backlog table
pn.Row( # First row with the first set of plots
plot_pane_13bis, # First plot
pn.Spacer(width=10), # Spacer
plot_pane_13bbis, # Second plot
),
pn.Spacer(height=50), # 10/07 20 --> 50
kpi_summary_card_backlog, # KPI summary for backlog below the second plot
pn.Spacer(height=10), # Optional vertical spacer between rows
),
vertical_divider_long, # Vertical divider separating the left and right columns
pn.Spacer(width=20),
pn.Column( # Right column for additional plots and KPI summaries
pn.Row( # Row for the third plot and KPI summary
plot_pane_15bis, # Third plot
pn.Spacer(width=10), # Spacer
kpi_summary_card, # KPI summary card
pn.Spacer(width=30), # Spacer
vertical_divider_med1, # Vertical divider for layout
pn.Spacer(width=30), # Spacer
plot_pane_14bis_2, # Last plot in this row
),
pn.Spacer(height=40), # 10/07
pn.Row( # Row for finance KPI card
pn.Spacer(height=20), # Optional spacer for vertical spacing
kpi_summary_card_finance # KPI finance table card
),
),
)
)
# Create a container with a max width constraint using pn.layout
container = pn.Column(
combined_plots_layout,
sizing_mode='stretch_width',
)
# Create a dashboard combining all plots with the title
combined_dashboard = pn.Column(
container,
sizing_mode='stretch_both' # Ensure the column stretches to fill available vertical space
)
##############################################################################################
# Initial call to update_widgets_and_table to populate the table based on default selections
#############################################################################################
production_dashboard = pn.Column(
pn.pane.HTML(f"""
<div style="text-align: left;">
<style>
h2 {{ margin-bottom: 0; color: #305496; }} /* Set title color here */
p {{ margin-top: 0; }}
</style>
<h2>Production</h2>
<p>{f"|CM-WIP| - <b>{file_date}</b>: Open WO at IDD based on QAD (ERP) | [Daily update]"}</p>
</div>
"""),
wip_selected_top,
filter_widgets_Prod,
wip_table,
height=600, # Set a fixed height to Production doashboard stays within the 600
sizing_mode='stretch_width' # Adjust sizing mode
)
#New 09/20
################################################################################
# Display the selected 'Pty Indice' & 'Drawing#' on the dashboard
#################################################################################
# Text widget to display the output
text_widget = pn.pane.HTML()
def trim_seda_top_level(seda_top_level):
"""
Trims the SEDA Top Level value to derive the drawing#.
"""
if pd.isnull(seda_top_level):
return ""
seda_top_level = str(seda_top_level)
# Case 1: Ends with - followed by digits (e.g., 3616-1, 2101-915-2, 851-11622-140)
if "-" in seda_top_level and seda_top_level.split("-")[-1].isdigit():
parts = seda_top_level.split("-")
if len(parts[-1]) == 1: # e.g., 3616-1
return "-".join(parts[:-1])
elif len(parts[-1]) == 2: # e.g., 2101-915-2
return "-".join(parts[:-1])
elif len(parts[-1]) == 3: # e.g., 851-11622-140
return seda_top_level[:-4]
# Case 2: Keep as is (e.g., 590010-1IE032)
return seda_top_level
def generate_display_text():
try:
# Get selected values from widgets
selected_indice = indice_widget.value
# Filter df_Priority based on the selected Pty Indice
if selected_indice == 'All':
filtered_rows = df_Priority # Include all rows if 'All' is selected
else:
if isinstance(selected_indice, list):
filtered_rows = df_Priority[df_Priority['Pty Indice'].isin(selected_indice)]
else:
filtered_rows = df_Priority[df_Priority['Pty Indice'] == selected_indice]
# Handle empty results
if filtered_rows.empty:
return "<span style='font-size: 18px; color: red;'>No matching data found</span>"
# Generate unique formatted strings
display_set = set()
for _, row in filtered_rows.iterrows():
idd_top_level = row['IDD Top Level']
seda_top_level = row['SEDA Top Level']
# Trim SEDA Top Level to derive drawing#
seda_trimmed = trim_seda_top_level(seda_top_level)
pty_indice = row['Pty Indice']
# Capitalize Pty Indice and IDD Top Level
pty_indice = pty_indice.upper()
idd_top_level = idd_top_level.upper()
# Create the display text
display_text = f"<b>{pty_indice} {idd_top_level}</b> (drawing# {seda_trimmed})"
display_set.add(display_text)
# Sort and group the display text
sorted_display_texts = sorted(display_set)
grouped_text = [
"; ".join(sorted_display_texts[i:i+3])
for i in range(0, len(sorted_display_texts), 3)
]
final_display_text = "<br>".join(grouped_text)
# Wrap the final text in the desired styling
return f"<span style='font-size: 18px; color: #32599E;'>{final_display_text}</span>"
except Exception as e:
return f"<span style='font-size: 18px; color: red;'>Error: {str(e)}</span>"
# Callback to update the text when widget values change
def update_text(event):
text_widget.object = generate_display_text()
# Attach the callback to the widgets
indice_widget.param.watch(update_text, 'value')
priority_widget.param.watch(update_text, 'value')
program_widget.param.watch(update_text, 'value')
# Initialize the text widget with default values
text_widget.object = generate_display_text()
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
###*********************#################********************##############*************************
####################################################################################################################
# Creating the complete dashboard
#####################################################################################################################
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
#########################################################
# Watch changes on widget
#########################################################
program_widget.param.watch(update_priorities, 'value')
priority_widget.param.watch(update_indices, 'value')
#########################################################
# Callbaks
#########################################################
# Define callbacks for widget events - update_changes_table
program_widget.param.watch(update_changes_table, 'value')
priority_widget.param.watch(update_changes_table, 'value')
indice_widget.param.watch(update_changes_table, 'value')
# Define callbacks for widget events - update_sales_table
program_widget.param.watch(update_sales_table, 'value')
priority_widget.param.watch(update_sales_table, 'value')
indice_widget.param.watch(update_sales_table, 'value')
# Define callbacks for widget events - update_sales_summary
program_widget.param.watch(update_sales_summary, 'value')
priority_widget.param.watch(update_sales_summary, 'value')
indice_widget.param.watch(update_sales_summary, 'value')
# Define callbacks for widget events - update_supply_table
program_widget.param.watch(update_supply_table, 'value')
priority_widget.param.watch(update_supply_table, 'value')
indice_widget.param.watch(update_supply_table, 'value')
# Define callbacks for widget events - update_turnover_table
program_widget.param.watch(update_turnover_table, 'value')
priority_widget.param.watch(update_turnover_table, 'value')
indice_widget.param.watch(update_turnover_table, 'value')
# Define callbacks for widget events - update_turnover_summary
program_widget.param.watch(update_turnover_summary, 'value')
priority_widget.param.watch(update_turnover_summary, 'value')
indice_widget.param.watch(update_turnover_summary, 'value')
# Define callbacks for widget events - update_supply_selected_top
program_widget.param.watch(update_supply_selected_top, 'value')
priority_widget.param.watch(update_supply_selected_top, 'value')
indice_widget.param.watch(update_supply_selected_top, 'value')
# Define callbacks for widget events - update_wip_selected_top
program_widget.param.watch(update_wip_selected_top, 'value')
priority_widget.param.watch(update_wip_selected_top, 'value')
indice_widget.param.watch(update_wip_selected_top, 'value')
# Update 09/16
# Set up callbacks for all widgets
for widget in [program_widget, priority_widget, indice_widget] + list(filters_Prod.values()):
widget.param.watch(widget_change_prod, 'value')
# New 09/03
# Set up callbacks for supply chain table purchased archi
program_widget.param.watch(on_widget_change_supply, 'value')
priority_widget.param.watch(on_widget_change_supply, 'value')
indice_widget.param.watch(on_widget_change_supply, 'value')
# Update 09/16
# Set up callbacks for supply chain table full archi
for widget in [program_widget, priority_widget, indice_widget] + list(filters_fullArchi.values()):
widget.param.watch(lambda event: update_supply_table_fullArchi(event), 'value')
# Link widget and plot update for Graph 13bis
program_widget.param.watch(update_plot_13bis, 'value')
priority_widget.param.watch(update_plot_13bis, 'value')
indice_widget.param.watch(update_plot_13bis, 'value')
# Link widget and plot update for Graph 13bbis
program_widget.param.watch(update_plot_13bbis, 'value')
priority_widget.param.watch(update_plot_13bbis, 'value')
indice_widget.param.watch(update_plot_13bbis, 'value')
# Link widget and plot update for Graph 14bis_2
program_widget.param.watch(update_plot_14bis_2, 'value')
priority_widget.param.watch(update_plot_14bis_2, 'value')
indice_widget.param.watch(update_plot_14bis_2, 'value')
# Link widget and plot update for Graph 14bbis
program_widget.param.watch(update_plot_14bbis, 'value')
priority_widget.param.watch(update_plot_14bbis, 'value')
indice_widget.param.watch(update_plot_14bbis, 'value')
# Link widget and plot update for Graph 14bbis
program_widget.param.watch(update_plot_15bis, 'value')
priority_widget.param.watch(update_plot_15bis, 'value')
indice_widget.param.watch(update_plot_15bis, 'value')
################################################################
# Define the cadrans_dashboard layout
################################################################
text_above_4cadrans_graphs = (
f"These graphs are based on data from |Snapshot| - <b>{file_date}</b>:<br>"
"▷ <b>Snapshot table</b>: Represents the remaining scope of the Transfer Project for the selected 'Program'.<br>"
"➥ It includes all PNs that have an existing IDD Backlog or for which the 'Critical Quantity,' defined as part of the transfer project, has not yet been reached. This applies even if the PN is not currently listed in the IDD Backlog.<br>"
"➥ Some PNs may not yet have an assigned IDD PN under 'IDD Top-Level'. In such cases, the BOM does not exist, and the given PN won't be present in this table; therefore, no data will be available for the graphs.<br>"
)
# Define your color
line_color = "#4472C4" # Change this to your desired color
font_top_color = "#4472C4"
#subtitle_backgroud_color = "#F2F2F2" #Gray
subtitle_background_color = "#aee0d9"
#------------------------------------------
# Convert the string to a datetime object using the format "%m-%d-%Y" to match the m-d-Y format
file_date_obj = datetime.strptime(file_date, "%m-%d-%Y")
# Format the datetime object into the desired m/d/Y format
formatted_date = file_date_obj.strftime("%m/%d/%Y")
# include within cadran title
cadrans_title = f"Status snapshot & 4 quadrant [{formatted_date}]"
#------------------------------------------
candrans_subtitle = "Selection of the Priority to be displayed on the dashboard"
candrans_subtitle2 = "Snapshot"
candrans_subtitle3 = " 4 quadrant - Engineering / Backlog / Supply Chain / Production"
# Create vertical and horizontal divs to act as colored lines
vertical_line = pn.pane.HTML(f"<div style='width: 6px; height: 800px; background-color: {line_color};'></div>")
horizontal_line = pn.pane.HTML(f"<div style='width: 2600px; height: 6px; background-color: {line_color};'></div>")
title_section = pn.pane.HTML(f"""
<div style='background-color: {font_top_color}; width: 100%; padding: 10px; box-sizing: border-box;'>
<h1 style='font-size: 24px; color: white; text-align: left; margin: 0;'>{cadrans_title}</h1>
</div>
""", sizing_mode='stretch_width')
# Title Layout
title_layout = pn.Column(
title_section,
pn.layout.Divider(margin=(-10, 0, 0, 0)), # Title divider
pn.Column(
pn.layout.Spacer(height=5), # Spacer to add space after subtitle
pn.Row(
program_widget,
priority_widget,
indice_widget,
pn.Column( # Group the spacer and text_widget inside a column for proper vertical spacing
pn.layout.Spacer(height=15), # Spacer to add space before text_widget
text_widget # Your formatted text widget
),
pn.layout.Spacer(height=10), # Spacer to add space after widget selection
sizing_mode='stretch_width' # Ensure the row stretches to fill the width
),
#pn.pane.HTML('<hr style="border: 1px solid #D9D9D9; margin: 10px 0;">'), # Custom horizontal line (break)
pn.Row(
filter_widgets_top_level_4_cadran, # Newly added 02/26, placed in a dedicated row
sizing_mode='stretch_width' # Ensure the row stretches to fill the width
),
sizing_mode='stretch_width' # Ensure the column stretches to fill the width
),
sizing_mode='stretch_width' # Ensure the title layout stretches to fill the width
)
# Define Secondary Layout
secondary_layout = pn.Column(
pn.pane.HTML(f"""
<div style='background-color: {subtitle_background_color};
width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 15px;'> <!-- Corrected closing div tag -->
<h1 style='font-size: 22px; color: white; text-align: left; margin: 0;'>
{candrans_subtitle2}
</h1>
</div>
""",sizing_mode='stretch_width'),
#pn.layout.Divider(margin=(-10, 0, 0, 0)), # Title divider
pn.Spacer(height=10), # Spacer before plots
text_above_4cadrans_graphs,
pn.Spacer(height=10), # Spacer before plots
combined_dashboard,
sizing_mode='stretch_width', # Ensure the secondary layout stretches to fill the width
height=1000 # Set a fixed height to prevent overlap # 10/07 920 --> 1000
)
# Define Primary Layout
primary_layout = pn.Column(
pn.Column(
pn.pane.HTML(f"""
<div style='background-color: {subtitle_background_color};
width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 15px;'> <!-- Corrected closing div tag -->
<h1 style='font-size: 22px; color: white; text-align: left; margin: 0;'>
{candrans_subtitle3}
</h1>
</div>
""",sizing_mode='stretch_width'),
#pn.layout.Divider(margin=(-10, 0, 0, 0)), # Title divider
pn.Row(
pn.Column(
changes_dashboard,
sizing_mode='stretch_width' # Adjust sizing mode for Engineering quadrant
),
vertical_line, # Add vertical line between columns
pn.Column(
production_dashboard,
sizing_mode='stretch_width' # Adjust sizing mode for Production quadrant
),
sizing_mode='stretch_width', # Adjust sizing mode for the entire row
),
horizontal_line, # Add horizontal line between upper and lower quadrants
pn.Row(
pn.Column(
supply_dashboard,
sizing_mode='stretch_both' # Adjust sizing mode for Supply Chain quadrant
),
vertical_line, # Add vertical line between columns
pn.Column(
sales_dashboard,
sizing_mode='stretch_both' # Adjust sizing mode for Sales quadrant
),
),
sizing_mode='stretch_both', # Adjust sizing mode for the entire primary layout
)
)
# Combine Title, Primary, and Secondary Layouts
cadrans_dashboard = pn.Column(
title_layout,
pn.layout.Divider(margin=(0, 0, -10, 0)), # Add some space between primary and secondary layouts if needed
secondary_layout,
pn.layout.Divider(margin=(0, 0, -10, 0)), # Add some space between primary and secondary layouts if needed
primary_layout,
sizing_mode='stretch_both' # Ensure the final layout stretches to fill available space
)
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# |Project Overview| - historic_dashboard
##############################################################################################################################
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
###############################################################
# Historic datafram
###############################################################
# Ensure 'Invoice date' is in datetime format
df_Historic['Invoice date'] = pd.to_datetime(df_Historic['Invoice date'])
#Updated 09/06 to replace 'Standard amount USD' with 'Currency turnover ex.VAT' which represents the sales
# Rename columns
df_Historic = df_Historic.rename(columns={
'Quantity': 'Quantity shipped',
'Currency turnover ex.VAT': 'Sales',
'Complexity': 'Average Complexity',
})
# Filter to exclude rows where 'Order' contains 'NC'
df_Historic = df_Historic[~df_Historic['Order'].str.contains('NC')]
##########################################
# Sorting df_Historic
###########################################
# Function to check if a value is numeric
def is_numeric(val):
try:
int(val)
return True
except ValueError:
return False
# Separate numeric and non-numeric 'Priority' values
df_numeric_priority = df_Historic[df_Historic['Priority'].apply(is_numeric)]
df_non_numeric_priority = df_Historic[~df_Historic['Priority'].apply(is_numeric)]
# Convert 'Priority' values to integers for numeric priorities
df_numeric_priority['Priority'] = df_numeric_priority['Priority'].astype(int)
# Sort numeric priorities in ascending order
#df_numeric_priority = df_numeric_priority.sort_values(by='Priority', ascending=True) - update 08/28
df_numeric_priority.sort_values(by=['Priority', 'Pty Indice'], inplace=True)
# Combine the DataFrames, placing numeric priorities first and non-numeric priorities at the end
df_Historic_sorted = pd.concat([df_numeric_priority, df_non_numeric_priority])
# Reset index if needed
df_Historic_sorted.reset_index(drop=True, inplace=True)
# Update the original DataFrame
df_Historic = df_Historic_sorted
#print('df_Historic:')
#display(df_Historic)
#####################################
# Group by 'Month' and 'Program'
########################################
monthly_summary = df_Historic.groupby(['Month', 'Program']).agg({
'Quantity shipped': 'sum',
'Sales': 'sum',
'Pty Indice': lambda x: ', '.join(map(str, x)),
'IDD Top Level': lambda x: ', '.join(map(str, x)), # 03/10
'SEDA Top Level': lambda x: ', '.join(map(str, x)), #03/10
'IDD Marge Standard': 'sum',
'Invoice date': 'first', # Keep the 'Invoice date' as the first date in each group
'Average Complexity': 'mean' # Calculate the average complexity
}).reset_index()
# Define a function to format numbers with 1 decimal digit if necessary
def format_complexity(value):
if pd.isna(value): # Handle NaN values
return value
elif value.is_integer():
return int(value) # Return as integer if value is an integer
else:
return round(value, 1) # Round to 1 decimal place otherwise
# Apply the formatting function to 'Complexity' column
monthly_summary['Average Complexity'] = monthly_summary['Average Complexity'].apply(format_complexity)
#Create 'Normalized Complexity'
monthly_summary['Normalized Complexity'] = monthly_summary['Average Complexity']*monthly_summary['Quantity shipped']
###################################
# Fill NaN values appropriately
###################################
# Fill numeric columns with 0
numeric_cols = monthly_summary.select_dtypes(include='number').columns
monthly_summary[numeric_cols] = monthly_summary[numeric_cols].fillna(0)
# Fill string columns with ''
string_cols = monthly_summary.select_dtypes(include='object').columns
monthly_summary[string_cols] = monthly_summary[string_cols].fillna('')
# Ensure 'Invoice date' is in datetime format
df_Historic['Invoice date'] = pd.to_datetime(df_Historic['Invoice date'])
# Sort by 'Invoice date' in descending order
monthly_summary = monthly_summary.sort_values(by='Invoice date', ascending=True)
# Display the updated DataFrame
#print('monthly_summary:')
#display(monthly_summary)
#################################################################################################################
# Widgets initialization
################################################################################################################
# Explicit list of allowed programs
allowed_programs_Historic = ['Phase 4-5', 'EMBRAER', 'SIKORSKY', 'COMAC', '1stTB', 'Space-X'] # 04/17
# Example usage with defaults
default_program_historic = 'Phase 4-5'
# Initialize program widget, excluding NaN values
#unique_programs_historic = df_Priority['Program'].dropna().unique().tolist()
# program_widget_historic = pn.widgets.Select(name='Select Program', options=unique_programs_historic, value=default_program_historic)
#----------------------------------- New 03/25 ------------------------------------------------------
# Create widget with only allowed programs
program_widget_historic = pn.widgets.Select(
name='Select Program',
options=allowed_programs_Historic,
value=default_program_historic
)
#------------------------------------------------------------------------------------------------------
program = program_widget_historic.value
#//////////////////////////////////////////////////
########################################
# Create Graphs Monthly_history
########################################
#//////////////////////////////////////////////////
# To review 09/06
# Custom color palette with alpha transparency
custom_palette = {
'Quantity shipped': '#5AB2CA',
'Sales': '#63BE7B',
'IDD Marge Standard': '#E2EFDA',
'Normalized Complexity': 'rgba(255, 47, 47, 0.7)' # Alpha applied
}
def customize_qty_shipped_plot(bokeh_plot):
""" Apply customizations to the Quantity Shipped plot. """
bokeh_plot.xaxis.major_label_text_font_size = '8pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.title.text_font_size = '12pt'
bokeh_plot.title.text_color = "#305496"
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 'vertical' # 10/23 change to vertical
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#E0E0E0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
return bokeh_plot
def customize_combined_plot(bokeh_plot):
""" Apply customizations to the Combined plot. """
bokeh_plot.xaxis.major_label_text_font_size = '8pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.title.text_font_size = '12pt'
bokeh_plot.title.text_color = "#305496"
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 'vertical' # 10/23 change to vertical
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#F0F0F0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
# Format the y-axis ticks in thousands with a dollar sign
bokeh_plot.yaxis.formatter =CustomJSTickFormatter(code="""
return '$' + (tick / 1000).toFixed(0) + 'k';
""")
return bokeh_plot
def customize_total_quantity_plot(bokeh_plot):
""" Apply customizations to the Total Quantity plot. """
bokeh_plot.xaxis.major_label_text_font_size = '8pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.title.text_font_size = '12pt'
bokeh_plot.title.text_color = "#305496"
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 'vertical'
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#E0E0E0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
return bokeh_plot
def create_total_quantity_plot(df_Historic, default_program_historic):
# Filter data by the default program
filtered_data = df_Historic[df_Historic['Program'] == default_program_historic]
if filtered_data.empty:
print("No data found for the default program.")
return None
# Aggregate data: Sum 'Quantity shipped' for each 'Pty Indice'
aggregated_data = filtered_data.groupby('Pty Indice')['Quantity shipped'].sum().reset_index()
# If the program is 'Phase 4-5', sort by 'Priority'
if default_program_historic == 'Phase 4-5':
# Merge the aggregated data with original to retain 'Priority'
aggregated_data = pd.merge(aggregated_data, filtered_data[['Pty Indice', 'Priority']].drop_duplicates(), on='Pty Indice')
# Convert 'Priority' values to integers for sorting
aggregated_data['Priority'] = aggregated_data['Priority'].astype(int)
# Sort numeric priorities in ascending order
#aggregated_data = aggregated_data.sort_values(by='Priority', ascending=True) - update 08/28
aggregated_data.sort_values(by=['Priority', 'Pty Indice'], inplace=True)
# Define the uniform color - 09/12
uniform_color = '#5AB2CA' # Light blue color
# Create the plot Total Quantity Shipped Monthly
total_quantity_plot = aggregated_data.hvplot.bar(
x='Pty Indice',
y='Quantity shipped',
title="Total Quantity Shipped Monthly",
xlabel='Pty Indice',
ylabel='Total Quantity Shipped',
#cmap=custom_palette,
color=uniform_color, # Apply the same color to all bars
legend='top_left',
height=400,
tools=[]
)
return total_quantity_plot
def create_plots(monthly_summary, default_program_historic, df_Historic):
# Filter data by default program
filtered_data = monthly_summary[monthly_summary['Program'] == default_program_historic]
if filtered_data.empty:
print("No data found for the default program.")
return None, None, None
# Melt the DataFrame to include Normalized Complexity
melted_df = filtered_data.melt(id_vars=['Month'], value_vars=['Quantity shipped', 'Sales', 'IDD Marge Standard', 'Normalized Complexity'],
var_name='Quantity Type', value_name='Quantity Value')
# Create plot for 'Quantity shipped' and 'Normalized Complexity'
qty_shipped_plot = melted_df[melted_df['Quantity Type'].isin(['Quantity shipped', 'Normalized Complexity'])].hvplot.bar(
x='Month',
y='Quantity Value',
color='Quantity Type',
title="Monthly shipment - Quantity Shipped & Normalized Complexity",
xlabel='Month',
ylabel='Top-Level shipped [Quantity]',
cmap=custom_palette,
legend='top_left',
height=400,
bar_width=0.6, # Set bar width - 09/12
tools=[]
)
bokeh_qty_shipped_plot = hv.render(qty_shipped_plot, backend='bokeh')
bokeh_qty_shipped_plot = customize_qty_shipped_plot(bokeh_qty_shipped_plot)
#New 08/09
#####################################################
# Remove existing HoverTools (if any) before adding a new one
bokeh_qty_shipped_plot.tools = [tool for tool in bokeh_qty_shipped_plot.tools if not isinstance(tool, HoverTool)]
# Add HoverTool with custom formatting
hover = HoverTool()
hover.tooltips = [
("Month", "@Month"),
("KPI", "@color"),
("Value", "@Quantity_Value")
]
# Add HoverTool to the plot
bokeh_qty_shipped_plot.add_tools(hover)
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
bokeh_qty_shipped_plot.tools = [tool for tool in bokeh_qty_shipped_plot.tools if not isinstance(tool, WheelZoomTool)]
############################################################
# Create combined plot for 'IDD Marge Standard' and 'Sales'
combined_plot = melted_df[melted_df['Quantity Type'].isin(['IDD Marge Standard', 'Sales'])].hvplot.bar(
x='Month',
y='Quantity Value',
color='Quantity Type',
title="Monthly shipment - IDD Margin & Total Sales",
xlabel='Month',
ylabel='[K$]',
cmap=custom_palette,
legend='top_left',
stacked=True, # Stacking bars
height=400,
bar_width=0.6, # Set bar width - 09/12
tools=[]
)
bokeh_combined_plot = hv.render(combined_plot, backend='bokeh')
bokeh_combined_plot = customize_combined_plot(bokeh_combined_plot)
#New 08/08
#####################################################
# Remove existing HoverTools (if any) before adding a new one
bokeh_combined_plot.tools = [tool for tool in bokeh_combined_plot.tools if not isinstance(tool, HoverTool)]
# Add HoverTool with custom formatting
hover = HoverTool()
hover.tooltips = [
("Month", "@Month"),
("KPI", "@color"),
("Value", "@Quantity_Value{($0,0k)}") # Format values: thousands with 'K' # Quantity_Value with the '_' otherwise that does not work!
]
# Add HoverTool to the plot
bokeh_combined_plot.add_tools(hover)
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
bokeh_combined_plot.tools = [tool for tool in bokeh_combined_plot.tools if not isinstance(tool, WheelZoomTool)]
############################################################
# Create Total Quantity Shipped plot
total_quantity_plot = create_total_quantity_plot(df_Historic, default_program_historic)
if total_quantity_plot:
bokeh_total_quantity_plot = hv.render(total_quantity_plot, backend='bokeh')
bokeh_total_quantity_plot = customize_total_quantity_plot(bokeh_total_quantity_plot)
else:
bokeh_total_quantity_plot = None
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
bokeh_total_quantity_plot.tools = [tool for tool in bokeh_total_quantity_plot.tools if not isinstance(tool, WheelZoomTool)]
return bokeh_qty_shipped_plot, bokeh_combined_plot, bokeh_total_quantity_plot
def update_plots(event):
# Get the selected program from the widget
program = program_widget_historic.value
#print(f"Updating plots for program: {program}")
# Filter data by the selected program
filtered_data = monthly_summary[monthly_summary['Program'] == program]
if filtered_data.empty:
print("No data found for the selected program.")
return
# Melt the DataFrame
melted_df = filtered_data.melt(id_vars=['Month'], value_vars=['Quantity shipped', 'Sales', 'IDD Marge Standard', 'Normalized Complexity'],
var_name='Quantity Type', value_name='Quantity Value')
# Update plots
bokeh_qty_shipped_plot, bokeh_combined_plot, bokeh_total_quantity_plot = create_plots(filtered_data, program, df_Historic)
# Update the plots in the Panel layout
plot_pane1.object = bokeh_qty_shipped_plot
plot_pane2.object = bokeh_combined_plot
plot_pane3.object = bokeh_total_quantity_plot
# Initial setup of the plots
bokeh_qty_shipped_plot, bokeh_combined_plot, bokeh_total_quantity_plot = create_plots(monthly_summary, default_program_historic, df_Historic)
# Convert Bokeh plots to Panel
plot_pane1 = pn.pane.Bokeh(bokeh_qty_shipped_plot, sizing_mode='stretch_width')
plot_pane2 = pn.pane.Bokeh(bokeh_combined_plot, sizing_mode='stretch_width')
plot_pane3 = pn.pane.Bokeh(bokeh_total_quantity_plot, sizing_mode='stretch_width')
# Update plot initially - Needed for the sizing_mode='stretch_width' to be set
update_plots(None)
############################################################################################
# Display the datafram monthly_summary of list of Pty Indice for each Month under Graph 3
#############################################################################################
# Function as to use dataframe df_Historic as monthly_summary does not have enough information
# Function to format Pty Indice with quantities from 'Total Qty Shipped'
def format_pty_indice_with_qty(row, df_Historic):
"""Format Pty Indice with actual quantities shipped per month.
Args:
row (pd.Series): A row from the DataFrame containing 'Month' and 'Program'.
df_Historic (pd.DataFrame): The historical DataFrame containing 'Month', 'Pty Indice', and 'Quantity shipped'.
Returns:
str: Formatted string with Pty Indices and their quantities shipped for the given month.
"""
try:
# Extract the month and program from the row
month = row['Month']
program = row['Program']
# Filter df_Historic for the given month and program
filtered_data = df_Historic[
(df_Historic['Month'] == month) &
(df_Historic['Program'] == program)
]
if filtered_data.empty:
return "No data found for this month and program."
# Group by Pty Indice and sum the quantities shipped
pty_indice_qty = filtered_data.groupby('Pty Indice')['Quantity shipped'].sum().reset_index()
# Format the output as "Pty Indice (Quantity shipped)"
formatted_output = [
f"{pty_indice} ({int(qty)} shipped)"
for pty_indice, qty in zip(pty_indice_qty['Pty Indice'], pty_indice_qty['Quantity shipped'])
]
return ', '.join(formatted_output)
except Exception as e:
print(f"Error processing row: {e}")
return "Format Error"
# Function to remove duplicates in comma-separated strings
def remove_duplicates_from_string(s):
if pd.isnull(s):
return s
items = s.split(', ')
unique_items = sorted(set(items), key=items.index) # Preserve order
return ', '.join(unique_items)
# Filter DataFrame by program - Update 03/11
def filter_dataframe_monthly_summary(program, df_Historic):
# Apply the filter based on the selected program
filtered_df = monthly_summary[monthly_summary['Program'] == program]
if filtered_df.empty:
print("No data found for the specified program.")
return filtered_df
# Apply the formatting function to each row
filtered_df['Pty Indice'] = filtered_df.apply(
lambda row: format_pty_indice_with_qty(row, df_Historic), axis=1
)
# Handle other columns and return the result
filtered_df = filtered_df[['Month', 'Pty Indice', 'Quantity shipped', 'IDD Top Level']]
filtered_df = filtered_df.rename(columns={'Quantity shipped': 'Total Qty Shipped'})
# Convert 'Month' column to datetime format
filtered_df['Month'] = pd.to_datetime(filtered_df['Month'], format='%b %y')
# Sort by 'Month' in descending order (most recent first)
filtered_df = filtered_df.sort_values(by='Month', ascending=False)
# Convert 'Month' back to the original string format (optional)
filtered_df['Month'] = filtered_df['Month'].dt.strftime('%b %y')
# Reorder 'IDD Top Level' based on 'Pty Indice' order
def reorder_idd_top_level(row, df_Historic):
month = row['Month']
pty_indices = [x.split(' ')[0] for x in row['Pty Indice'].split(', ')] # Extract Pty Indices
# Filter df_Historic for the given month and Pty Indices
filtered_data = df_Historic[
(df_Historic['Month'] == month) &
(df_Historic['Pty Indice'].isin(pty_indices))
]
if filtered_data.empty:
return row['IDD Top Level']
# Group by Pty Indice and collect IDD Top Level values
idd_mapping = filtered_data.groupby('Pty Indice')['IDD Top Level'].unique().reset_index()
# Create a dictionary to map Pty Indice to IDD Top Level
idd_dict = dict(zip(idd_mapping['Pty Indice'], idd_mapping['IDD Top Level']))
# Reorder IDD Top Level based on Pty Indice order
ordered_idd = []
for pty in pty_indices:
if pty in idd_dict:
ordered_idd.extend(idd_dict[pty])
# Join the ordered IDD Top Level values
return ', '.join(ordered_idd)
# Apply the reordering function to each row
filtered_df['IDD Top Level'] = filtered_df.apply(
lambda row: reorder_idd_top_level(row, df_Historic), axis=1
)
# Remove duplicates in 'IDD Top Level' column
filtered_df['IDD Top Level'] = filtered_df['IDD Top Level'].apply(remove_duplicates_from_string)
return filtered_df
#####################################################
# Table colored in bleu and white every other rows
#######################################################
def style_dataframe_bleu(df):
"""Apply custom styles to the DataFrame (alternating row colors and centered text).
Args:
df (pd.DataFrame): The DataFrame to style.
Returns:
pd.io.formats.style.Styler: Styled DataFrame.
"""
def row_styles(row):
"""Apply alternating row colors."""
color = '#ADDAE5' if row.name % 2 == 0 else '#ffffff'
return [f'background-color: {color}'] * len(row)
# Reset the index to ensure it is sequential
df = df.reset_index(drop=True)
# Create a Styler object
styled_df = df.style
# Apply alternating row colors
styled_df = styled_df.apply(row_styles, axis=1)
# Center text in specific columns
styled_df = styled_df.set_properties(
subset=['Month', 'Total Qty Shipped'], # Columns to center
**{'text-align': 'center'} # CSS property for centering text
)
# Hide the index
styled_df.hide(axis="index")
return styled_df
# Function to update the DataFrame summary with custom styling
def update_dataframe_monthly_summary(program, df_Historic):
"""Update the DataFrame summary with custom styling.
Args:
program (str): The selected program.
df_Historic (pd.DataFrame): The historical DataFrame.
Returns:
str: HTML representation of the styled DataFrame.
"""
filtered_df = filter_dataframe_monthly_summary(program, df_Historic)
styled_df = style_dataframe_bleu(filtered_df)
styled_html = styled_df.to_html() # Convert styled DataFrame to HTML
# Add CSS for overflow handling directly in the HTML
html_with_overflow = f'<div style="overflow-y: auto; height: 450px;">{styled_html}</div>'
return html_with_overflow
# Callback function to update the table based on widget value
def update_table(event):
"""Callback function to update the table based on the selected program."""
# Get the selected program from the widget
program = program_widget_historic.value
# Update the DataFrame pane with the new styled DataFrame
monthly_summary_table.object = update_dataframe_monthly_summary(program, df_Historic)
# Attach callback to the widget
program_widget_historic.param.watch(update_table, 'value')
# Create initial DataFrame table
monthly_summary_table = pn.pane.HTML(
update_dataframe_monthly_summary(default_program_historic, df_Historic),
width=1000
)
######################################
# Create text bellow graphs
########################################
# Convert 'Invoice date' to datetime format
df_Historic['Invoice date'] = pd.to_datetime(df_Historic['Invoice date'])
# Calculate the span period of the Turnover Report span_TurnoverReport
start_date_historic = df_Historic['Invoice date'].min()
end_date_historic = df_Historic['Invoice date'].max()
span_df_Historic = f"{start_date_historic.date()} to {end_date_historic.date()}" # Format dates as needed
text_below_graph_qty_shipped_plot = (
f"This graph is based on data from |CM-Historic| - Span: <b>{span_df_Historic}</b>:<br>"
f"▷ <b>Quantity shipped</b>: Total quantity of Top-Level related to the selected program shipped since {start_date_historic.date()}<br>"
"▷ <b>Normalized Complexity</b>: Average complexity of the Top-Level shipped normalized on the quantity of each PN shipped on the period.<br>"
"▷ <b>The complexity is define as</b>: Kit, Subs = 0, Lighplate = 1, Rotottelite = 2, CPA = 3, ISP = 4.<br>"
)
text_below_graph_Marge_Sales = (
f"This graph is based on data from |CM-Historic| - Span: <b>{span_df_Historic}</b>:<br>"
"▷ <b>Sales</b>: Sum of the 'Currency turnover ex.VAT' for the PN shipped during the specified month<br>"
"▷ <b>IDD Marge Standard</b>: Sum of the 'IDD Margin Standard' for the PN shipped during the specified month.<br>"
)
text_below_graph_shipped_pty_indice = (
f"This graph is based on data from |CM-Historic| - Span: <b>{span_df_Historic}</b>:<br>"
f"▷ <b>Total Quantity shipped</b>: Total quantity of Top-Level related to the selected pty Indice shipped since {start_date_historic.date()}.<br>"
)
###WORKING CODE
##############################################
# Combine plots into a vertical Panel layout
###############################################
#create short vertical divider
vertical_divider_medium = pn.pane.HTML(
'<div style="width: 1px; height: 500px; background-color:#D9D9D9;"></div>',
)
vertical_divider_medium2 = pn.pane.HTML(
'<div style="width: 1px; height: 500px; background-color:#D9D9D9;"></div>',
)
# Combine the plots and table in the layout
combined_plots_history = pn.Column(
pn.Row(
pn.Column(
plot_pane1,
text_below_graph_qty_shipped_plot
),
pn.Spacer(width=50),
vertical_divider_medium,
pn.Spacer(width=50),
pn.Column(
plot_pane2,
text_below_graph_Marge_Sales
),
sizing_mode='stretch_width'
),
pn.Spacer(height=50), # Spacer before the next row
pn.Row(
pn.Column(
plot_pane3,
text_below_graph_shipped_pty_indice
),
monthly_summary_table, # Add table pane to the right of the text and plot
)
)
########################################
# Call-out on program_widget_historic
########################################
program_widget_historic.param.watch(update_plots, 'value')
#//////////////////////////////////////////////////
###################################################
# Create Graphs Categories of products - 08/09
###################################################
#Load df_Priority as it has been filtered previously on the code
df_Priority = pd.read_excel(input_file_formatted, sheet_name='CM-Priority', index_col=False)
#----------------------------------------------------------
# 02/11 - Change 'Phase 4' or 'Phase 5' with 'Phase 4-5'
#----------------------------------------------------------
# For df_Priority
if 'Program' in df_Priority.columns and 'Pty Indice' in df_Priority.columns:
mask = (
df_Priority['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Priority['Pty Indice'].str.contains('Phase5', na=False)
)
df_Priority.loc[mask, 'Program'] = 'Phase 4-5'
#----------------------------------------------------------
#------------------------------------------------------------------------------------------
# 03/19 - Only keep 'Program' = 'Phase 4-5', 'EMBRAER', SIKORSKY', '1stTB', '2ndTB'
#------------------------------------------------------------------------------------------
# Define the allowed values for the 'Program' column
allowed_programs = ['Phase 4-5', 'EMBRAER', 'SIKORSKY', 'COMAC', '1stTB', '2ndTB']
# Apply filtering to each DataFrame
df_Priority = df_Priority[df_Priority['Program'].isin(allowed_programs)]
#------------------------------------------------------------------------------------------
# Correctly accessing multiple columns
Pivot_table_distribution = df_Snapshot[['Pty Indice', 'Top-Level Status', 'Priority', 'Shipped', 'Remain. crit. Qty', 'Production Status', 'IDD Backlog Qty', 'Product Category', 'Critical Qty']]
# Apply mapping to create 'Program' column in df_WIP
Pivot_table_distribution['Program'] = Pivot_table_distribution['Pty Indice'].map(indice_to_program)
#Include missing Pty from df_Priority and fill 'IDD Backlog Qty' with 0
# Perform a left join, so all rows from Pivot_table_distribution are kept
Pivot_table_distribution = pd.merge(
Pivot_table_distribution,
df_Priority[['Pty Indice']].drop_duplicates(), # Selecting only the 'Pty Indice' column from df_Priority and dropping duplicates
on='Pty Indice',
how='left' # 'left' join to keep all rows from Pivot_table_distribution
)
#Fill 'IDD Backlog Qty' with 0 where missing
Pivot_table_distribution['IDD Backlog Qty'].fillna(0, inplace=True)
# Calculate necessary fields - Update 08/14: 'Total Quantity' should be set as 'IDD Backlog Qty' + 'Shipped' not just 'IDD Backlog Qty'
#Pivot_table_distribution['Total Quantity'] = Pivot_table_distribution[['Critical Qty', 'IDD Backlog Qty']].max(axis=1)
# Ensure that there are no NaN values in 'Shipped' and 'Remain. crit. Qty'
Pivot_table_distribution['Shipped'].fillna(0, inplace=True)
Pivot_table_distribution['Remain. crit. Qty'].fillna(0, inplace=True)
# Compute the sum of 'IDD Backlog Qty' and 'Shipped'
Pivot_table_distribution['Sum IDD Backlog and Shipped'] = Pivot_table_distribution['IDD Backlog Qty'] + Pivot_table_distribution['Shipped']
# Apply conditional logic: Total Quantity' = 'IDD Backlog Qty' + 'Shipped' ONLY if 'Remain. crit. Qty' = 0
Pivot_table_distribution['Total Quantity'] = np.where(
Pivot_table_distribution['Remain. crit. Qty'] == 0,
Pivot_table_distribution['Sum IDD Backlog and Shipped'],
Pivot_table_distribution[['Critical Qty', 'IDD Backlog Qty']].max(axis=1)
)
# Optionally, drop the intermediate column if no longer needed
#Pivot_table_distribution.drop(columns=['Sum IDD Backlog and Shipped'], inplace=True)
#print("Pivot_table_distribution:")
#display(Pivot_table_distribution)
#Saved 08/13 to include redlist in these tables
'''
###################################
# Aggregate data by Program and Product Category
aggregation_by_product_category = Pivot_table_distribution.groupby(['Program', 'Product Category']) \
.agg(Pty_Indice_Count_Product_Category=('Pty Indice', 'nunique')) \
.reset_index()
# Aggregate data by Program and Production Status
aggregation_by_production_status = Pivot_table_distribution.groupby(['Program', 'Production Status']) \
.agg(Pty_Indice_Count_Production_Status=('Pty Indice', 'nunique')) \
.reset_index()
#########################
# Print aggregated data
#########################
#print("Aggregated Data by Product Category:")
#display(aggregation_by_product_category)
#print("Aggregated Data by Production Status:")
#display(aggregation_by_production_status)
'''
#New 08/12
######################################################################################
# Define the % Completion of each Pty Indice in a new dataframe Pivot_table_completion
##########################################################################################
# Copy Pivot_table_distribution
Pivot_table_completion = Pivot_table_distribution.copy()
#print('Pivot_table_completion before adding new rows')
#display(Pivot_table_completion)
######################################################################################################################
# df_Priority is missing ['Top-Level Status', 'IDD Backlog Qty', 'Product Category', 'Total Quantity'] --> delete 'Top-Level Status', 'Product Category' & 'IDD Backlog Qty' from Dataframe
# If PN is not in df_snapshot and therefor not in Pivot_table_completion, it should mean that either:
# --> The PN does not have a BOM: Either not a Top-Level or prep work not completed.
# --> The PN is part of the redlist & 'Top-Level Status' = short or 'IDD Backlog Qty' = 0 or both: Filtered-out from df_Snapshot
# --> The values should be set as: = 'Total Quantity' ='Critical Qty'
####################################################################################################################
# Columns to remove from Pivot_table_completion
columns_to_remove = ['Top-Level Status', 'Production Status', 'IDD Backlog Qty', 'Product Category']
Pivot_table_completion = Pivot_table_completion.drop(columns=columns_to_remove, errors='ignore')
# Create mappings from df_Priority
priority_mapping = df_Priority.set_index('Pty Indice')['Priority']
program_mapping = df_Priority.set_index('Pty Indice')['Program']
shipped_mapping = df_Priority.set_index('Pty Indice')['Shipped']
# Map values to Pivot_table_completion
Pivot_table_completion['Priority'] = Pivot_table_completion['Pty Indice'].map(priority_mapping)
Pivot_table_completion['Program'] = Pivot_table_completion['Pty Indice'].map(program_mapping)
Pivot_table_completion['Qty Shipped'] = Pivot_table_completion['Pty Indice'].map(shipped_mapping)
# Fill missing values for 'Priority' and 'Program'
Pivot_table_completion['Priority'].fillna('Unknown', inplace=True)
Pivot_table_completion['Program'].fillna('Unknown', inplace=True)
# Optionally, filter out rows where 'Qty Shipped' is NaN or 0
Pivot_table_completion = Pivot_table_completion[Pivot_table_completion['Qty Shipped'].notna() & (Pivot_table_completion['Qty Shipped'] > 0)]
# Define criteria for including additional rows from df_Priority
# For example, you might want to include rows where 'Qty Shipped' > a certain threshold
additional_rows_criteria = df_Priority['Shipped'] > 0 # Example condition
additional_rows = df_Priority[additional_rows_criteria]
# Add only the rows that are not already in Pivot_table_completion based on 'Pty Indice'
additional_rows = additional_rows[~additional_rows['Pty Indice'].isin(Pivot_table_completion['Pty Indice'])]
# Select and rename columns to match Pivot_table_completion
additional_rows = additional_rows[['Pty Indice', 'Shipped', 'Priority', 'Program', 'Critical Qty']]
additional_rows.rename(columns={'Shipped': 'Qty Shipped'}, inplace=True)
# Append additional rows to Pivot_table_completion
Pivot_table_completion = pd.concat([Pivot_table_completion, additional_rows], ignore_index=True)
# Optionally, remove duplicates if necessary
Pivot_table_completion = Pivot_table_completion.drop_duplicates(subset='Pty Indice', keep='last')
# Fill 'Total Quantity' with 'Critical Qty' where 'Total Quantity' is NaN -- Update 08/14: When 'IDD Backlog' > 0, the 'Total Quantity' = 'IDD Backlog' + 'Qty Shipped'
Pivot_table_completion['Total Quantity'] = Pivot_table_completion['Total Quantity'].fillna(Pivot_table_completion['Critical Qty'])
#Sort by 'Priority' end place string at the end of the datafram
# Convert 'Priority' column to numeric, coercing errors (non-numeric entries become NaN)
Pivot_table_completion['Priority'] = pd.to_numeric(Pivot_table_completion['Priority'], errors='coerce')
# Fill NaNs with a default value (e.g., 999) for sorting
Pivot_table_completion['Priority'].fillna(999, inplace=True)
# Sort the DataFrame by 'Priority' column in ascending order
#Pivot_table_completion.sort_values(by='Priority', ascending=True, inplace=True) - Update 08/28
Pivot_table_completion.sort_values(by=['Priority', 'Pty Indice'], inplace=True)
# Reset index if needed
Pivot_table_completion.reset_index(drop=True, inplace=True)
#print('Pivot_table_completion with missing rows, no duplicates, and updated columns:')
#display(Pivot_table_completion)
########################################################################################
# Define % Conpletion Critical Qty = Critical Qty / Qty Shipped
Pivot_table_completion['% Completion Critical Qty'] = (Pivot_table_completion['Qty Shipped'] / Pivot_table_completion['Critical Qty']) * 100
# Cap the values at 100%
Pivot_table_completion['% Completion Critical Qty'] = Pivot_table_completion['% Completion Critical Qty'].clip(upper=100)
# Replace NaN values with 0
Pivot_table_completion['% Completion Critical Qty'] = Pivot_table_completion['% Completion Critical Qty'].fillna(0)
# Round to the nearest whole number
Pivot_table_completion['% Completion Critical Qty'] = Pivot_table_completion['% Completion Critical Qty'].round(0).astype(int)
# Replace NaN values with 0
Pivot_table_completion['% Completion Critical Qty'] = Pivot_table_completion['% Completion Critical Qty'].fillna(0)
# Define % Completion Total Qty = Total Qty / Qty Shipped
Pivot_table_completion['% Completion Total Qty'] = (Pivot_table_completion['Qty Shipped'] / Pivot_table_completion['Total Quantity']) * 100
# Replace NaN values with 0
Pivot_table_completion['% Completion Total Qty'] = Pivot_table_completion['% Completion Total Qty'].fillna(0)
# Round to the nearest whole number
#Pivot_table_completion['% Completion Total Qty'] = Pivot_table_completion['% Completion Total Qty'].round(0).astype(int) # saved 02/03
##### 02/03 ####################################
# First handle infinite values and NaNs
Pivot_table_completion['% Completion Total Qty'] = (
Pivot_table_completion['% Completion Total Qty']
.replace([np.inf, -np.inf], np.nan) # Replace infinities with NaN
.fillna(0) # Now fill all NaNs with 0
)
# Then perform rounding and conversion
Pivot_table_completion['% Completion Total Qty'] = (
Pivot_table_completion['% Completion Total Qty']
.round(0)
.astype(int)
)
####################################################
###############################
# Print Pivot_table_completion
################################
# Set options to display the entire DataFrame
pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
#print('Pivot_table_completion with ALL column')
#display(Pivot_table_completion)
#############################################################################################################
# New 08/13 - Include product from the redlist 'To be transferred' or 'Canceled' or 'Officially transferred' within Pivot_table_completion
##############################################################################################################
Pivot_table_scope = df_Priority.copy()
#print('df_Priority without any filter')
#display(Pivot_table_scope)
#Function to get the 'Product Category' based on 'Description'
# Define the 'Product Category' based on 'Description'
def determine_category(description):
if not isinstance(description, str):
return 'Others'
if description == 'Rototellite':
return 'Rototellite'
elif 'Indicator' in description or 'CPA' in description:
return 'CPA'
elif 'Lightplate' in description:
return 'Lightplate'
elif 'ISP' in description or 'Keyboard' in description:
return 'ISP'
elif 'Module' in description:
return 'CPA'
elif 'optics' in description:
return 'Fiber Optics'
else:
return 'Others'
# Create 'Product Category' column based on the 'Description'
Pivot_table_scope['Product Category'] = Pivot_table_scope['Description'].apply(determine_category)
#Include 'Total Quantity' in Pivot_table_scope from Pivot_table_completion otherwise set 'Total Quantity' = 'Critical Qty'
# Merge with Pivot_table_completion to get 'Total Quantity'
Pivot_table_scope = Pivot_table_scope.merge(
Pivot_table_completion[['Pty Indice', 'Total Quantity']],
on='Pty Indice',
how='left'
)
# Fill in 'Total Quantity' where missing with 'Critical Qty'
Pivot_table_scope['Total Quantity'] = Pivot_table_scope['Total Quantity'].fillna(Pivot_table_scope['Critical Qty'])
#Keep only relevant column
Pivot_table_scope = Pivot_table_scope[['Pty Indice', 'Product Category', 'Critical Qty', 'Production Status', 'Total Quantity', 'Program']]
#print('Pivot_table_scope')
#display(Pivot_table_scope)
#################################################################################################
# Filter-out 'Canceled' PN from aggreagated data to exclude the canceled order from the scope
#################################################################################################
# Filter out rows where 'Production Status' is 'Canceled' OR 'Critical Qty' is 'To be canceled'
Pivot_table_scope_filtered = Pivot_table_scope[
~((Pivot_table_scope['Production Status'] == 'Canceled') |
(Pivot_table_scope['Critical Qty'] == 'To be canceled'))
]
######################################
#Ordering 'Product Category' from Pivot_table_scope_filtered: ISP, CPA, Lightplate, Rototellite, Others, Fiber Optics
#####################################
# Define the desired order for 'Product Category'
category_order = ['ISP', 'CPA', 'Lightplate', 'Rototellite', 'Others', 'Fiber Optics']
# Convert 'Product Category' to a categorical type with the specified order
Pivot_table_scope_filtered['Product Category'] = pd.Categorical(
Pivot_table_scope_filtered['Product Category'],
categories=category_order,
ordered=True
)
# Sort the DataFrame by 'Product Category'
Pivot_table_scope_filtered = Pivot_table_scope_filtered.sort_values(by='Product Category')
#print('Pivot_table_scope_filtered')
#display(Pivot_table_scope_filtered)
#######################################
# Aggregation on 'Product Category' and 'program', summing 'Total Quantity'
Pivot_table_scope_filtered_aggregated = Pivot_table_scope_filtered.groupby(['Product Category', 'Program'])['Total Quantity'].sum().reset_index()
#print('Pivot_table_scope_filtered_aggregated')
#display(Pivot_table_scope_filtered_aggregated)
# Aggregate data by Program and Product Category - Filtered to exclude canceled orders
aggregation_by_product_category = Pivot_table_scope_filtered.groupby(['Program', 'Product Category']) \
.agg(Pty_Indice_Count_Product_Category=('Pty Indice', 'nunique')) \
.reset_index()
# Aggregate data by Program and Production Status - Not filtered to include canceled orders
aggregation_by_production_status = Pivot_table_scope.groupby(['Program', 'Production Status']) \
.agg(Pty_Indice_Count_Production_Status=('Pty Indice', 'nunique')) \
.reset_index()
# Ordering aggregation_by_production_status by 'Production Status': Completed, Industrialized, FTB WIP, Proto WIP, FTB, Proto + FTB, To be transferred, Canceled, 'Officially transferred'
# Define the desired order for 'Production Status'
status_order = ['Completed', 'Industrialized', 'FTB WIP', 'Proto WIP', 'FTB', 'Proto + FTB', 'To be transferred', 'Canceled', 'Officially transferred']
# Convert 'Production Status' to a categorical type with the specified order
aggregation_by_production_status['Production Status'] = pd.Categorical(
aggregation_by_production_status['Production Status'],
categories=status_order,
ordered=True
)
# Sort the DataFrame by 'Production Status'
aggregation_by_production_status = aggregation_by_production_status.sort_values(by='Production Status')
#########################
# Print aggregated data
#########################
#print("Aggregated Data by Product Category - With redlist:")
#display(aggregation_by_product_category)
#print("Aggregated Data by Production Status - With redlist:")
#display(aggregation_by_production_status)
#New 08/14
#####################################################################################################################################
#### Include 'Combined PN' representing the full Transfer Project
## Pivot_table_completion contain only PN shipped, all the other PN are therefor at %0
## Get the other PN from CM-Priority (filtering-out 'Canceled') --> Pivot_table_scope_filtered does not contain the 'Canceled' PN but contain 'To be transferred'
####################################################################################################################################
# Merge Pivot_table_completion with Pivot_table_scope_filtered on 'Pty Indice' to get the '% Completion Total Qty' and '% Completion Critical Qty'
# Select only the relevant columns from Pivot_table_completion
completion_filtered = Pivot_table_completion[['Priority', 'Pty Indice', 'Qty Shipped', '% Completion Total Qty', '% Completion Critical Qty']]
# Merge to keep the column from Pivot_table_scope_filtered and include the missing from Pivot_table_completion
Pivot_table_completion_upated = pd.merge(Pivot_table_scope_filtered, completion_filtered, on='Pty Indice', how='left')
# Set 'NaN' from '% Completion Total Qty' and '% Completion Critical Qty' to 0
Pivot_table_completion_upated['% Completion Total Qty'] = Pivot_table_completion_upated['% Completion Total Qty'].fillna(0)
Pivot_table_completion_upated['% Completion Critical Qty'] = Pivot_table_completion_upated['% Completion Critical Qty'].fillna(0)
Pivot_table_completion_upated['Qty Shipped'] = Pivot_table_completion_upated['Qty Shipped'].fillna(0)
# Create a Priority Mapping from CM-Protity to Pivot_table_completion_upated on 'Pty Indice' for row with 'Pty Indice = NaN
priority_mapping = df_Priority.set_index('Pty Indice')['Priority']
#Identify rows with NaN Priority
missing_priority_mask = Pivot_table_completion_upated['Priority'].isna()
#Apply the Priority Mapping only to rows with NaN Priority
Pivot_table_completion_upated.loc[missing_priority_mask, 'Priority'] = Pivot_table_completion_upated.loc[missing_priority_mask, 'Pty Indice'].map(priority_mapping)
# Identify string values in 'Priority' column
string_mask = Pivot_table_completion_upated['Priority'].apply(lambda x: isinstance(x, str))
# Replace string values with 999
Pivot_table_completion_upated.loc[string_mask, 'Priority'] = 999
# Drop NaN values # 03/11
Pivot_table_completion_upated = Pivot_table_completion_upated.dropna(subset=['Priority'])
# Convert columns from float to integer
Pivot_table_completion_upated['Priority'] = Pivot_table_completion_upated['Priority'].astype(int)
Pivot_table_completion_upated['Total Quantity'] = Pivot_table_completion_upated['Total Quantity'].astype(int)
Pivot_table_completion_upated['Qty Shipped'] = Pivot_table_completion_upated['Qty Shipped'].astype(int)
# Round the percentage columns to one decimal place
Pivot_table_completion_upated['% Completion Total Qty'] = Pivot_table_completion_upated['% Completion Total Qty'].round(1)
Pivot_table_completion_upated['% Completion Critical Qty'] = Pivot_table_completion_upated['% Completion Critical Qty'].round(1)
# Create an empty list to store the new rows
new_rows = []
# Group by 'Program'
grouped = Pivot_table_completion_upated.groupby('Program')
# Calculate aggregated values for each group
for program, group in grouped:
total_quantity_sum = group['Total Quantity'].sum()
total_shipped_sum = group['Qty Shipped'].sum()
#average_completion_total_qty = group['% Completion Total Qty'].mean()
#average_completion_critical_qty = group['% Completion Critical Qty'].mean()
total_critical_qty = group['Critical Qty'].sum()
# Calculate completion percentages
# Handle division by zero cases by checking if total values are greater than zero
completion_total_qty = (total_shipped_sum / total_quantity_sum) * 100 if total_quantity_sum > 0 else 0
completion_critical_qty = (total_shipped_sum / total_critical_qty) * 100 if total_critical_qty > 0 else 0
# Create a new row for each program
new_row = {
'Pty Indice': f'Combined PN {program}',
'Priority': 98,
'Program': program,
'Product Category': 'Made up row, combined PN',
'Qty Shipped': total_shipped_sum,
'Critical Qty': total_critical_qty,
'Total Quantity': total_quantity_sum,
'% Completion Total Qty': completion_total_qty,
'% Completion Critical Qty': completion_critical_qty,
'Production Status': 'Made up row, combined PN'
}
# Append the new row to the list
new_rows.append(new_row)
# Convert the list of new rows into a DataFrame
new_rows_df = pd.DataFrame(new_rows)
# Append the new rows to the existing DataFrame
Pivot_table_completion_upated_combinedPN = pd.concat([Pivot_table_completion_upated, new_rows_df], ignore_index=True)
# Round the percentage columns to one decimal place
Pivot_table_completion_upated_combinedPN['% Completion Total Qty'] = Pivot_table_completion_upated_combinedPN['% Completion Total Qty'].round(1)
Pivot_table_completion_upated_combinedPN['% Completion Critical Qty'] = Pivot_table_completion_upated_combinedPN['% Completion Critical Qty'].round(1)
# Sort the DataFrame by 'Priority' - update 08/28
Pivot_table_completion_upated = Pivot_table_completion_upated.sort_values(by='Priority')
Pivot_table_completion_upated_combinedPN = Pivot_table_completion_upated_combinedPN.sort_values(by='Priority')
Pivot_table_completion_upated = Pivot_table_completion_upated.sort_values(by=['Priority', 'Pty Indice'])
Pivot_table_completion_upated_combinedPN = Pivot_table_completion_upated_combinedPN.sort_values(by=['Priority', 'Pty Indice'])
#print('Pivot_table_completion_upated')
#display(Pivot_table_completion_upated)
#print('Pivot_table_completion_upated_combinedPN')
#display(Pivot_table_completion_upated_combinedPN)
#New 08/14
###############################################################################################################
# Create new datafram Pourcentage_distribution based on Pivot_table_scope_filtered (canceled order excluded)
###############################################################################################################
#Copy Pivot_table_scope_filtered
Pourcentage_distribution = Pivot_table_scope_filtered.copy()
#print('Pourcentage_distribution')
#display(Pourcentage_distribution)
# Function to round numeric columns except the 'Program' column
def round_except_program(df, decimals=0, int_columns=None):
# Select columns to round (exclude 'Program')
numeric_columns = df.columns[df.columns != 'Program']
if int_columns:
# Convert specified columns to integer
df[int_columns] = df[int_columns].astype(int)
# Round remaining columns
df[numeric_columns] = df[numeric_columns].round(decimals)
return df
#########################################
# Group by TOTAL Qty 'Productin Status'
##########################################
# Group by 'Program' and 'Product Status'
status_distribution = Pourcentage_distribution.groupby(['Program', 'Production Status'])['Total Quantity'].sum().reset_index()
# Calculate percentage
status_total = status_distribution.groupby('Program')['Total Quantity'].transform('sum')
status_distribution['Percentage'] = (status_distribution['Total Quantity'] / status_total) * 100
# Pivot the table
status_distribution_pivot = status_distribution.pivot_table(
index='Program',
columns='Production Status',
values='Total Quantity',
aggfunc='sum',
fill_value=0
)
status_percentage_pivot = status_distribution.pivot_table(
index='Program',
columns='Production Status',
values='Percentage',
aggfunc='sum',
fill_value=0
)
# Resetting index for clarity
status_distribution_pivot = status_distribution_pivot.reset_index()
status_percentage_pivot = status_percentage_pivot.reset_index()
# Apply the function
#status_distribution_pivot = round_except_program(status_distribution_pivot, int_columns=['Completed', 'FTB', 'FTB WIP', 'Industrialized', 'Proto + FTB', 'Proto WIP', 'To be transferred', 'Officially transferred'])
status_distribution_pivot = round_except_program(status_distribution_pivot, int_columns=['Completed', 'FTB', 'FTB WIP', 'Industrialized', 'Proto + FTB', 'Proto WIP', 'Officially transferred'])
status_percentage_pivot = round_except_program(status_percentage_pivot, decimals=1)
#print
#print('status_distribution _pivot')
#display(status_distribution_pivot)
#print('status_percentage_pivot')
#display(status_percentage_pivot)
#####################################
# Group by TOTAL Qty Product Category'
#####################################
# Group by 'Program' and 'Product Category'
category_distribution = Pourcentage_distribution.groupby(['Program', 'Product Category'])['Total Quantity'].sum().reset_index()
# Calculate percentage
category_total = category_distribution.groupby('Program')['Total Quantity'].transform('sum')
category_distribution['Percentage'] = (category_distribution['Total Quantity'] / category_total) * 100
# Pivot the table
category_distribution_pivot = category_distribution.pivot_table(
index='Program',
columns='Product Category',
values='Total Quantity',
aggfunc='sum',
fill_value=0
)
category_percentage_pivot = category_distribution.pivot_table(
index='Program',
columns='Product Category',
values='Percentage',
aggfunc='sum',
fill_value=0
)
# Resetting index for clarity
category_distribution_pivot = category_distribution_pivot.reset_index()
category_percentage_pivot = category_percentage_pivot.reset_index()
# Apply the function for rounding
category_distribution_pivot = round_except_program(category_distribution_pivot, int_columns=['ISP', 'CPA', 'Lightplate', 'Rototellite', 'Others', 'Fiber Optics'])
category_percentage_pivot = round_except_program(category_percentage_pivot, decimals=1)
# print
#print('category_distribution_pivot')
#display(category_distribution_pivot)
############################################################################################################################
# Melt category_percentage_pivot & status_percentage_pivot - For Chart 4 - Distribution by TOTAL Quantity
###############################################################################################################################
# Melt the DataFrame to long format
df_melted_status_percentage_pivot = status_percentage_pivot.melt(id_vars=['Program'], var_name='Production Status', value_name='Percentage Status')
df_melted_category_percentage_pivot = category_percentage_pivot.melt(id_vars=['Program'], var_name='Product Category', value_name='Percentage Status')
#print('df_melted_status_percentage_pivot')
#display(df_melted_status_percentage_pivot)
# New 08/16
#########################################################
# - For Chart 5 - Distribution by UNIQUE Top-Level
########################################################
#Copy Pivot_table_scope_filtered
Pourcentage_distribution_Unique = Pivot_table_scope_filtered.copy()
##############################
# Group by UNIQUE 'Production Status'
###############################
# Group by 'Program' and 'Production Status'
status_distribution_unique = Pourcentage_distribution_Unique.groupby(['Program', 'Production Status'])['Pty Indice'].nunique().reset_index()
# Calculate total unique count for each Program
status_total_unique = status_distribution_unique.groupby('Program')['Pty Indice'].transform('sum')
# Calculate percentage
status_distribution_unique['Percentage'] = (status_distribution_unique['Pty Indice'] / status_total_unique) * 100
# Pivot the table
status_distribution_unique_pivot = status_distribution_unique.pivot_table(
index='Program',
columns='Production Status',
values='Pty Indice',
aggfunc='sum',
fill_value=0
)
status_percentage_unique_pivot = status_distribution_unique.pivot_table(
index='Program',
columns='Production Status',
values='Percentage',
aggfunc='sum',
fill_value=0
)
# Resetting index for clarity
status_distribution_unique_pivot = status_distribution_unique_pivot.reset_index()
status_percentage_unique_pivot = status_percentage_unique_pivot.reset_index()
# Apply the function for rounding
status_distribution_unique_pivot = round_except_program(status_distribution_unique_pivot, int_columns=['Completed', 'FTB', 'FTB WIP', 'Industrialized', 'Proto + FTB', 'Proto WIP', 'Officially transferred'])
status_percentage_unique_pivot = round_except_program(status_percentage_unique_pivot, decimals=1)
# print
#print('status_percentage_unique_pivot')
#display(status_percentage_unique_pivot)
##############################
# Group by UNIQUE 'Product Category'
###############################
# Group by 'Program' and 'Product Category'
category_distribution_unique = Pourcentage_distribution_Unique.groupby(['Program', 'Product Category'])['Pty Indice'].nunique().reset_index()
# Calculate total unique count for each Program
category_total_unique = category_distribution_unique.groupby('Program')['Pty Indice'].transform('sum')
# Calculate percentage
category_distribution_unique['Percentage'] = (category_distribution_unique['Pty Indice'] / category_total_unique) * 100
# Pivot the table
category_distribution_unique_pivot = category_distribution_unique.pivot_table(
index='Program',
columns='Product Category',
values='Pty Indice',
aggfunc='sum',
fill_value=0
)
category_percentage_unique_pivot = category_distribution_unique.pivot_table(
index='Program',
columns='Product Category',
values='Percentage',
aggfunc='sum',
fill_value=0
)
# Resetting index for clarity
category_distribution_unique_pivot = category_distribution_unique_pivot.reset_index()
category_percentage_unique_pivot = category_percentage_unique_pivot.reset_index()
# Apply the function for rounding
category_distribution_unique_pivot = round_except_program(category_distribution_unique_pivot, int_columns=['ISP', 'CPA', 'Lightplate', 'Rototellite', 'Others', 'Fiber Optics'])
category_percentage_unique_pivot = round_except_program(category_percentage_unique_pivot, decimals=1)
# print
#print('category_percentage_unique_pivot')
#display(category_percentage_unique_pivot)
############################################################################################################################
# Melt category_percentage_pivot & status_percentage_pivot - For Chart 5 - Distribution by UNIQUE Tpop-Level
###############################################################################################################################
# Melt the DataFrame to long format
#df_melted_status_percentage_pivot_UNIQUE = status_percentage_unique_pivot.melt(id_vars=['Program'], var_name='Production Status Unique', value_name='Percentage Status Unique')
#df_melted_category_percentage_pivot = category_distribution_unique_pivot.melt(id_vars=['Program'], var_name='Product Category Unique', value_name='Percentage Status Unique')
#print('df_melted_status_percentage_pivot_UNIQUE')
#display(df_melted_status_percentage_pivot_UNIQUE)
# Update on 18/16 to include Crossfiltering
#//////////////////////////////////////////////////#//////////////////////////////////////////////////#/////////////////////////////////////
##########################################################################################################################################
# Chart 1 - Percentage distribution of Product Categories --> Based on dataframe 'aggregation_by_product_category'
# Representing of each unique IDD Top-Level for each categories
##########################################################################################################################################
# Chart 2 - Percentage distribution of unique Top-Level by Product Category --> Based on dataframe 'Pivot_table_scope_filtered_aggregated'
#Representing of each category based on of the Total quantity to Build of each Pty Indice including in each categories
##########################################################################################################################################
# Chart 3 - Percentage distribution of Production Status --> Based on dataframe 'aggregation_by_production_status'
#Representing of each Production Status based on each unique Pty Indice
##########################################################################################################################################
#//////////////////////////////////////////////////#//////////////////////////////////////////////////#/////////////////////////////////////
#########################################################################################
#//////////////////////////////////////////////////#////////////////////////////////////
# Bar charts of product categories distribution and production statuses distribution
#########################################################################################
#//////////////////////////////////////////////////#////////////////////////////////////
#-------------------------------
# BEGINNING of WIP Code Update - 04/16
#-------------------------------
#-----------------------------------------------------------------------------------------------------------------------------------------------
# Define colors mapping if used
colors_palette = {
'CPA': '#a2d5d6',
'ISP': '#64179d',
'Lightplate': '#dfddda',
'Others': '#cfa8cf',
'Rototellite': '#233b3f',
'Fiber Optics': '#ea9770',
'Completed': 'green',
'FTB': '#9EC0F6',
'FTB WIP': '#DAEEF3',
'Industrialized': '#a7d0ac',
'Proto + FTB': '#6199ea',
'Proto WIP': '#00d3ff',
'To be transferred': '#F2DCDB',
'Officially transferred': '#FF7A5B',
'Canceled': '#F35757',
}
# Customize distribution plot
def customize_distribution_plot(bokeh_plot):
""" Apply customizations to the Total Quantity plot. """
bokeh_plot.xaxis.major_label_text_font_size = '10pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 45
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#E0E0E0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
return bokeh_plot
###########################
# Bar chart 1
###########################
def create_product_category(aggregation_by_product_category, program):
# Filter data by the default program
filtered_data = aggregation_by_product_category[aggregation_by_product_category['Program'] == program]
if filtered_data.empty:
print("No data found for the default program.")
return None
# Sort the filtered data by 'Pty_Indice_Count_Product_Category'
filtered_data = filtered_data.sort_values(by='Pty_Indice_Count_Product_Category')
# Create the plot
product_category_plot = filtered_data.hvplot.bar(
x='Product Category',
y='Pty_Indice_Count_Product_Category',
title=None, # Remove the built-in title
xlabel='Product Category',
ylabel='Nb of unique Top-Level',
cmap=colors_palette,
color='Product Category',
legend='top_left',
bar_width=0.6, # Set bar width
tools=[]
)
# Render the plot to Bokeh
bokeh_product_category_plot = hv.render(product_category_plot, backend='bokeh')
# Set explicit dimensions for the Bokeh plot
bokeh_product_category_plot.width = 400
bokeh_product_category_plot.height = 600
# Apply customizations
bokeh_product_category_plot = customize_distribution_plot(bokeh_product_category_plot)
# Remove borders from the bars
for renderer in bokeh_product_category_plot.renderers:
if isinstance(renderer.glyph, VBar):
renderer.glyph.line_color = None
# Create the HTML formatted title using Div
title_text = """<span style="font-size: 16px; color: #305496; margin-left: 60px;">
<b>Unique</b> Top-Level <b>Quantity</b> by Product Category
</span>"""
title_product_category_plot = Div(text=title_text)
# Remove existing HoverTools and add a new one
bokeh_product_category_plot.tools = [tool for tool in bokeh_product_category_plot.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Product Category", "@Product_Category"),
("Unique PN", "@Pty_Indice_Count_Product_Category")
]
bokeh_product_category_plot.add_tools(hover)
# Remove wheel zoom from active tools
bokeh_product_category_plot.tools = [tool for tool in bokeh_product_category_plot.tools if not isinstance(tool, WheelZoomTool)]
# Combine title and plot into a Panel Column layout
product_category_plot_layout = pn.Column(title_product_category_plot, bokeh_product_category_plot)
return product_category_plot_layout
# Create the initial plot
product_category_plot_layout = create_product_category(aggregation_by_product_category, default_program_historic)
bar_plot_pane1 = pn.panel(product_category_plot_layout, sizing_mode='fixed', height=600, width=400)
# Define the update function
def update_plots_product_category(event):
program = event.new if event else program_widget_historic.value
print(f"Selected Program: {program}")
filtered_data = aggregation_by_product_category[aggregation_by_product_category['Program'] == program]
print(f"Filtered Data: {filtered_data.head()}")
if filtered_data.empty:
print("No data found for the selected program.")
plot1_layout[:] = [pn.pane.HTML("<b>No data available for the selected program.</b>"), pn.Spacer(height=30), text_below_product_category]
return
product_category_plot_layout = create_product_category(filtered_data, program)
plot1_layout[:] = [product_category_plot_layout, pn.Spacer(height=30), text_below_product_category]
print("Plot Updated in Layout")
# Link the widget to the update function
program_widget_historic.param.watch(update_plots_product_category, 'value')
#######################################################
# Bar chart 2 - Pivot_table_scope_filtered_aggregated
########################################################
def create_product_category_total_qty(Pivot_table_scope_filtered_aggregated, program):
# Filter data by the default program
filtered_data = Pivot_table_scope_filtered_aggregated[Pivot_table_scope_filtered_aggregated['Program'] == program]
if filtered_data.empty:
print("No data found for the default program.")
return None
# Sort the filtered data by 'Total Quantity'
filtered_data = filtered_data.sort_values(by='Total Quantity')
# Create the plot
product_category_total_qty = filtered_data.hvplot.bar(
x='Product Category',
y='Total Quantity',
title=None, # Remove the built-in title
xlabel='Product Category',
ylabel='Total quantity of Top-Level',
cmap=colors_palette,
color='Product Category',
legend='top_left',
bar_width=0.6, # Set bar width
tools=[]
)
# Render the plot to Bokeh
bokeh_product_category_total_qty_plot = hv.render(product_category_total_qty, backend='bokeh')
# Set explicit dimensions for the Bokeh plot
bokeh_product_category_total_qty_plot.width = 400
bokeh_product_category_total_qty_plot.height = 600
# Apply customizations
bokeh_product_category_total_qty_plot = customize_distribution_plot(bokeh_product_category_total_qty_plot)
# Remove borders from the bars
for renderer in bokeh_product_category_total_qty_plot.renderers:
if isinstance(renderer.glyph, VBar):
renderer.glyph.line_color = None
# Create the HTML formatted title using Div
title_text = """<span style="font-size: 16px; color: #305496; margin-left: 60px;">
<b>Total</b> Top-Level <b>Quantity</b> by Product Category
</span>"""
title_product_category_total_qty_plot = Div(text=title_text)
# Remove existing HoverTools and add a new one
bokeh_product_category_total_qty_plot.tools = [tool for tool in bokeh_product_category_total_qty_plot.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Product Category", "@Product_Category"),
("Total Quantity", "@Total_Quantity")
]
bokeh_product_category_total_qty_plot.add_tools(hover)
# Remove wheel zoom from active tools
bokeh_product_category_total_qty_plot.tools = [tool for tool in bokeh_product_category_total_qty_plot.tools if not isinstance(tool, WheelZoomTool)]
# Combine title and plot into a Panel Column layout
product_category_total_qty_layout = pn.Column(title_product_category_total_qty_plot, bokeh_product_category_total_qty_plot)
return product_category_total_qty_layout
#--------------------------------------------------------------------
# TITLE NOT UPDATING
# Bar Chart 1: Unique Top-Level Quantity by Product Category
def update_plots_product_category_total_qty(event):
program = event.new if event else program_widget_historic.value
print(f"Selected Program: {program}")
filtered_data = Pivot_table_scope_filtered_aggregated[Pivot_table_scope_filtered_aggregated['Program'] == program]
print(f"Filtered Data: {filtered_data.head()}")
if filtered_data.empty:
print("No data found for the selected program.")
plot2_layout[:] = [pn.pane.HTML("<b>No data available for the selected program.</b>"), pn.Spacer(height=30), text_below_product_category_total_qty]
return
# Recreate the full layout with title
product_category_total_qty_layout = create_product_category_total_qty(filtered_data, program)
if product_category_total_qty_layout is not None:
plot2_layout[:] = product_category_total_qty_layout.objects + [pn.Spacer(height=30), text_below_product_category_total_qty]
print("Plot and title updated in layout")
else:
print("Failed to create product_category_total_qty_layout")
# Link the widget to the update function
program_widget_historic.param.watch(update_plots_product_category_total_qty, 'value')
####################################################################
# Bar chart 3 - Production Status, Pty_Indice_Count_Production_Status
######################################################################
def create_production_status(aggregation_by_production_status, program):
# Filter data by the default program
filtered_data = aggregation_by_production_status[aggregation_by_production_status['Program'] == program]
if filtered_data.empty:
print("No data found for the default program.")
return pn.pane.HTML("<b>No data available for the selected program.</b>")
# Get all unique production statuses from the entire dataset
all_production_statuses = aggregation_by_production_status['Production Status'].dropna().unique()
# Create a pivot table or aggregate data for the filtered program
status_counts = filtered_data.groupby('Production Status')['Pty_Indice_Count_Production_Status'].sum().reset_index()
# Create a complete DataFrame with all statuses
status_counts_complete = pd.DataFrame({
'Production Status': all_production_statuses
}).merge(status_counts, on='Production Status', how='left')
# Fill missing numeric values with 0, ensuring 'Production Status' remains categorical
status_counts_complete['Pty_Indice_Count_Production_Status'] = status_counts_complete['Pty_Indice_Count_Production_Status'].fillna(0).astype(float)
# Sort by count for better visualization
status_counts_complete = status_counts_complete.sort_values(by='Pty_Indice_Count_Production_Status', ascending=False)
# Create the plot
production_status_plot = status_counts_complete.hvplot.bar(
x='Production Status',
y='Pty_Indice_Count_Production_Status',
title=None,
xlabel='Production Status',
ylabel='Nb of unique Top-Level',
cmap=colors_palette,
color='Production Status',
legend='top_left',
bar_width=0.6,
tools=[]
)
# Render the plot to Bokeh
bokeh_production_status_plot = hv.render(production_status_plot, backend='bokeh')
# Set explicit dimensions for the Bokeh plot
bokeh_production_status_plot.width = 600
bokeh_production_status_plot.height = 600
# Apply customizations
bokeh_production_status_plot = customize_distribution_plot(bokeh_production_status_plot)
# Remove borders from the bars
for renderer in bokeh_production_status_plot.renderers:
if isinstance(renderer.glyph, VBar):
renderer.glyph.line_color = None
# Create the HTML formatted title using Div
title_text = """<span style="font-size: 16px; color: #305496; margin-left: 60px;">
<b>Unique</b> Top-level <b>Quantity</b> by Production Status
</span>"""
title_production_status_plot = Div(text=title_text)
# Remove existing HoverTools and add a new one
bokeh_production_status_plot.tools = [tool for tool in bokeh_production_status_plot.tools if not isinstance(tool, HoverTool)]
hover = HoverTool()
hover.tooltips = [
("Production Status", "@Production_Status"),
("Unique PN", "@Pty_Indice_Count_Production_Status")
]
bokeh_production_status_plot.add_tools(hover)
# Remove wheel zoom from active tools
bokeh_production_status_plot.tools = [tool for tool in bokeh_production_status_plot.tools if not isinstance(tool, WheelZoomTool)]
# Combine title and plot into a Panel Column layout
production_status_layout = pn.Column(title_production_status_plot, bokeh_production_status_plot)
return production_status_layout
#--------------------------------------------------------------------
# TITLE NOT UPDATING
# Bar Chart 3: Unique Top-Level Quantity by Production Status
def update_plots_production_status(event):
program = event.new if event else program_widget_historic.value
print(f"Selected Program: {program}")
filtered_data = aggregation_by_production_status[aggregation_by_production_status['Program'] == program]
print(f"Filtered Data: {filtered_data.head()}")
if filtered_data.empty:
print("No data found for the selected program.")
plot3_layout[:] = [pn.pane.HTML("<b>No data available for the selected program.</b>"), pn.Spacer(height=30), text_below_production_status]
return
# Recreate the full layout with title
production_status_layout = create_production_status(filtered_data, program)
if production_status_layout is not None:
plot3_layout[:] = production_status_layout.objects + [pn.Spacer(height=30), text_below_production_status]
print("Plot and title updated in layout")
else:
print("Failed to create production_status_layout")
# Link the widget to the update function
program_widget_historic.param.watch(update_plots_production_status, 'value')
#######################
# Create initial plots
########################
# Create the initial production status layout
product_category_total_qty_layout = create_product_category_total_qty(Pivot_table_scope_filtered_aggregated, default_program_historic)
production_status_layout = create_production_status(aggregation_by_production_status, default_program_historic)
# Create Bokeh plot panes with fixed dimensions
bar_plot_pane2 = pn.panel(product_category_total_qty_layout, sizing_mode='fixed', height=600, width=400)
bar_plot_pane3 = pn.panel(production_status_layout, sizing_mode='fixed', height=600, width=600)
#-------------------------------
# End of WIP Code Update - 04/16
#-------------------------------
######################################
# Create text bellow graphs
########################################
# Convert 'Last Update' to datetime format
df_Priority['Last Update'] = pd.to_datetime(df_Priority['Last Update'], format='%m/%d/%Y', errors='coerce')
# Format the date as a short date (MM-DD-YYYY)
df_Priority['Last Update'] = df_Priority['Last Update'].dt.strftime('%m-%d-%Y')
# Extract the single date value from the DataFrame
Date_CM_Priority = df_Priority['Last Update'].iloc[0]
text_below_product_category = pn.pane.HTML(
f"This graph is based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>Unique Top-Level Quantity </b>: Number of unique Part Number for each Category of Product.<br>"
"▷ <b>This graph includes</b>: All PN since the beginning of the transfer for the selected program included the 'redlist'.<br>"
"▷ <b>Redlist</b>: PN 'to be transferred' or 'Officially transferred', not yet 'Canceled' - Canceled order are filtered-out of this graph.<br>",
width=450 # Match the width of bar_plot_pane1 and bar_plot_pane2
)
text_below_product_category_total_qty = pn.pane.HTML(
f"This graph is based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>Total Quantity Top-Level</b>: Total Quantity of parts either in SEDA's Backlog for each category. The follow-up orders are included in this 'Total Quantity'.<br>"
"▷ <b>This graph includes</b>: All PN since the beginning of the transfer for the selected program included the 'redlist' (not yet canceled).<br>"
"▷ <b>Redlist</b>: PN 'to be transferred' or Officially transferred, not yet 'Canceled' - Canceled order are filtered-out of this graph.<br>",
width=450 # Match the width of bar_plot_pane2
)
text_below_production_status = pn.pane.HTML(
f"This graph is based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>Unique Quantity Top-Level </b>: Number of unique Part Number for each Category of Product.<br>"
"▷ <b>This graph includes</b>: All PN since the beginning of the transfer for the selected program included the 'redlist' & Canceled order.<br>",
width=600 # Match the width of bar_plot_pane3
)
#New 08/15
###########################################################################################################################################################################################
# Chart 4 - Percentage distribution of Product Category & Production Status --> Base on intial dataframe 'Pourcentage_distribution'
#Representing the % each product category and Proudction status based on TOTAL QTY Top-LeveL --> Dataframes df_melted_status_percentage_pivot & df_melted_category_percentage_pivot
##########################################################################################################################################################################################
# Combinaison of Dataframes df_melted_status_percentage_pivot & df_melted_category_percentage_pivot on create_percentage_product_category_production_status
# Updated 09/17
def create_percentage_product_category_production_status(df_melted_status_percentage_pivot, df_melted_category_percentage_pivot, program, colors_palette):
# Filter data by the default program
filtered_data_status = df_melted_status_percentage_pivot[df_melted_status_percentage_pivot['Program'] == program]
filtered_data_category = df_melted_category_percentage_pivot[df_melted_category_percentage_pivot['Program'] == program]
# Ensure 'Percentage Status' is numeric, fill NaN values with 0
filtered_data_status['Percentage Status'] = pd.to_numeric(filtered_data_status['Percentage Status'], errors='coerce').fillna(0)
filtered_data_category['Percentage Status'] = pd.to_numeric(filtered_data_category['Percentage Status'], errors='coerce').fillna(0)
# Convert categorical columns to categorical type
filtered_data_status['Production Status'] = pd.Categorical(filtered_data_status['Production Status'])
filtered_data_category['Product Category'] = pd.Categorical(filtered_data_category['Product Category'])
# Sort data by 'Percentage Status' by ascending order (smallest to largest)
filtered_data_status = filtered_data_status.sort_values(by='Percentage Status', ascending=False)
filtered_data_category = filtered_data_category.sort_values(by='Percentage Status', ascending=False)
# Extract the categories and corresponding colors
categories_status = filtered_data_status['Production Status'].cat.categories
categories_category = filtered_data_category['Product Category'].cat.categories
# Ensure the palette covers all categories
colors_palette_status = [colors_palette.get(cat, '#808080') for cat in categories_status]
colors_palette_category = [colors_palette.get(cat, '#808080') for cat in categories_category]
# Convert DataFrame to ColumnDataSource
source_status = ColumnDataSource(filtered_data_status)
source_category = ColumnDataSource(filtered_data_category)
# HTML title for Product Category
title_text_status_distribution = """<span style="font-size: 16px; color: #305496; margin-left: 150px;">
Production Status <b>Distribution</b> based on <b>Total Quantity</b>
</span>"""
title_status_plot_distribution = Div(text=title_text_status_distribution)
# Create figure for Production Status
status_plot = figure(
y_range=FactorRange(*filtered_data_status['Production Status']),
x_axis_label='Percentage Production Status',
y_axis_label='Production Status',
x_range=(0, 100), # Set x-axis range from 0 to 100
title=None
)
# Rename columns just before configuring HoverTool
filtered_data_status_renamed = filtered_data_status.rename(columns={
'Production Status': 'Production_Status',
'Percentage Status': 'Percentage_Status'
})
source_status_renamed = ColumnDataSource(filtered_data_status_renamed)
# Remove existing hover tool (if any) and create a new one
status_plot.tools = [tool for tool in status_plot.tools if not isinstance(tool, HoverTool)]
hover_status = HoverTool(
tooltips=[
("Production Status", "@Production_Status"),
("Percentage", "@Percentage_Status{0.0f}%")
]
)
status_plot.add_tools(hover_status)
status_plot.hbar(
y='Production_Status',
right='Percentage_Status',
source=source_status_renamed,
height=0.6, # Thickness of each bar
color=factor_cmap('Production_Status', palette=colors_palette_status, factors=categories_status),
legend_field='Production_Status' # Add legend field
)
# Set fixed dimensions directly here
status_plot.width = 600
status_plot.height = 600
# HTML title for Product Category
title_text_category_distribution = """<span style="font-size: 16px; color: #305496; margin-left: 50px;">
Product Category <b>Distribution</b> based on <b>Total Quantity</b>
</span>"""
title_category_plot_distribution = Div(text=title_text_category_distribution)
# Create figure for Product Category
category_plot = figure(
y_range=FactorRange(*filtered_data_category['Product Category']),
x_axis_label='Percentage Product Category',
y_axis_label='Product Category',
x_range=(0, 100), # Set x-axis range from 0 to 100
title=None
)
# Rename columns just before configuring HoverTool
filtered_data_category_renamed = filtered_data_category.rename(columns={
'Product Category': 'Product_Category',
'Percentage Status': 'Percentage_Status'
})
source_category_renamed = ColumnDataSource(filtered_data_category_renamed)
# Remove existing hover tool (if any) and create a new one
category_plot.tools = [tool for tool in category_plot.tools if not isinstance(tool, HoverTool)]
hover_category = HoverTool(
tooltips=[
("Product Category", "@Product_Category"),
("Percentage", "@Percentage_Status{0.0f}%")
]
)
category_plot.add_tools(hover_category)
category_plot.hbar(
y='Product_Category',
right='Percentage_Status',
source=source_category_renamed,
height=0.6, # Thickness of each bar
color=factor_cmap('Product_Category', palette=colors_palette_category, factors=categories_category),
legend_field='Product_Category' # Add legend field
)
category_plot.legend.location = 'top_right' # Position the legend in the top right
status_plot.legend.location = 'top_right' # Position the legend in the top right
# Apply customization for consistency with other graphs
status_plot = customize_distribution_plot(status_plot)
category_plot = customize_distribution_plot(category_plot)
# Customize grid lines
status_plot.xgrid.grid_line_color = '#E0E0E0'
status_plot.xgrid.grid_line_dash = [4, 6] # Dash style for x-axis grid lines
status_plot.ygrid.grid_line_color = None # Remove y-axis grid lines
category_plot.xgrid.grid_line_color = '#E0E0E0'
category_plot.xgrid.grid_line_dash = [4, 6] # Dash style for x-axis grid lines
category_plot.ygrid.grid_line_color = None # Remove y-axis grid lines
# Set fixed dimensions directly here
category_plot.width = 450
category_plot.height = 600
# Arrange plots and titles in columns
status_layout_distribution = column(title_status_plot_distribution, status_plot)
category_layout_distribution = column(title_category_plot_distribution, category_plot)
#return status_plot_unique, category_plot_unique # Update 10/07
return status_layout_distribution, category_layout_distribution
def update_plot_percentage_product_category_production_status(event):
program = program_widget_historic.value
# Filter data by the selected program
filtered_data_status = df_melted_status_percentage_pivot[df_melted_status_percentage_pivot['Program'] == program]
filtered_data_category = df_melted_category_percentage_pivot[df_melted_category_percentage_pivot['Program'] == program]
if filtered_data_status.empty or filtered_data_category.empty:
print("No data found for the selected program.")
return
# Get the plot layout
status_layout_distribution, category_layout_distribution = create_percentage_product_category_production_status(
df_melted_status_percentage_pivot,
df_melted_category_percentage_pivot,
program,
colors_palette # Make sure colors_palette is passed here
)
# Extract the actual plot (assuming it's the second child in the layout)
bokeh_status_plot = status_layout_distribution.children[1] # Accessing the second child (the plot)
bokeh_category_plot = category_layout_distribution.children[1] # Accessing the second child (the plot)
# Customize grid lines
#bokeh_status_plot.xgrid.grid_line_color = '#E0E0E0'
#bokeh_status_plot.xgrid.grid_line_dash = [4, 6] # Dash style for x-axis grid lines
#bokeh_status_plot.ygrid.grid_line_color = None # Remove y-axis grid lines
#bokeh_category_plot.xgrid.grid_line_color = '#E0E0E0'
#bokeh_category_plot.xgrid.grid_line_dash = [4, 6] # Dash style for x-axis grid lines
#bokeh_category_plot.ygrid.grid_line_color = None # Remove y-axis grid lines
# Set fixed dimensions
bokeh_status_plot.width = 600
bokeh_status_plot.height = 600
bokeh_category_plot.width = 450
bokeh_category_plot.height = 600
# Update Bokeh plot panes
bar_plot_pane_status_percentage.object = bokeh_status_plot
bar_plot_pane_category_percentage.object = bokeh_category_plot
#######################
# Create initial plots
########################
bokeh_status_plot, bokeh_category_plot = create_percentage_product_category_production_status(
df_melted_status_percentage_pivot,
df_melted_category_percentage_pivot,
default_program_historic,
colors_palette # Make sure colors_palette is passed here
)
# Create Bokeh plot panes with fixed dimensions
bar_plot_pane_status_percentage = pn.pane.Bokeh(bokeh_status_plot, sizing_mode='fixed', height=600, width=600)
bar_plot_pane_category_percentage = pn.pane.Bokeh(bokeh_category_plot, sizing_mode='fixed', height=600, width=450)
#update_plot_percentage_product_category_production_status(None)
####################################################
# Watch the widget and update the plot on change
####################################################
program_widget_historic.param.watch(update_plot_percentage_product_category_production_status, 'value')
######################################################################################################################################################################################
# Chart 5 - Percentage distribution of Product Category & Production Status --> Base on intial dataframe 'Pourcentage_distribution'
#Representing the % each product category and Proudction status based on UNIQUE Top-Level --> Dataframes df_melted_status_percentage_pivot & df_melted_category_percentage_pivot
#######################################################################################################################################################################################
# Updated 09/17
def create_percentage_product_category_production_status_UNIQUE(status_percentage_unique_pivot, category_percentage_unique_pivot, program, colors_palette):
#program = default_program_historic
# Filter data by the default program
filtered_data_status = status_percentage_unique_pivot[status_percentage_unique_pivot['Program'] == program]
filtered_data_category = category_percentage_unique_pivot[category_percentage_unique_pivot['Program'] == program]
if filtered_data_status.empty or filtered_data_category.empty:
print("No data found for the default program.")
return None, None
# Transform filtered_data_status into the desired format
transformed_status = filtered_data_status.melt(
id_vars=['Program'],
var_name='Production Status',
value_name='Percentage'
)
# Transform filtered_data_category into the desired format
transformed_category = filtered_data_category.melt(
id_vars=['Program'],
var_name='Product Category',
value_name='Percentage'
)
# Ensure 'Percentage' is numeric, fill NaN values with 0
transformed_status['Percentage'] = pd.to_numeric(transformed_status['Percentage'], errors='coerce').fillna(0)
transformed_category['Percentage'] = pd.to_numeric(transformed_category['Percentage'], errors='coerce').fillna(0)
# Sort data by 'Percentage' in descending order
transformed_status = transformed_status.sort_values(by='Percentage', ascending=False)
transformed_category = transformed_category.sort_values(by='Percentage', ascending=False)
# Convert categorical columns to categorical type
transformed_status['Production Status'] = pd.Categorical(transformed_status['Production Status'])
transformed_category['Product Category'] = pd.Categorical(transformed_category['Product Category'])
# Extract the categories and corresponding colors
categories_status = transformed_status['Production Status'].cat.categories
categories_category = transformed_category['Product Category'].cat.categories
# Ensure the palette covers all categories
colors_palette_status = [colors_palette.get(cat, '#808080') for cat in categories_status]
colors_palette_category = [colors_palette.get(cat, '#808080') for cat in categories_category]
# Convert DataFrame to ColumnDataSource
source_status = ColumnDataSource(transformed_status)
source_category = ColumnDataSource(transformed_category)
# Rename columns for hover tool compatibility
transformed_status_renamed = transformed_status.rename(columns={
'Production Status': 'Production_Status',
'Percentage': 'Percentage_Status'
})
transformed_category_renamed = transformed_category.rename(columns={
'Product Category': 'Product_Category',
'Percentage': 'Percentage_Status'
})
# Convert renamed DataFrames to ColumnDataSource
source_status_renamed = ColumnDataSource(transformed_status_renamed)
source_category_renamed = ColumnDataSource(transformed_category_renamed)
# HTML title for Production Status - margin-left creates a gap before the title
title_text_status = """<span style="font-size: 16px; color: #305496; margin-left: 130px;">
Production Status <b>Distribution</b> based on <b>Unique</b> Top-Level
</span>"""
title_status_plot_unique = Div(text=title_text_status)
# Create figure for Production Status without title
status_plot_unique = figure(
y_range=FactorRange(*transformed_status['Production Status']),
x_axis_label='Percentage Production Status',
y_axis_label='Production Status',
x_range=(0, 100),
title=None
)
# Remove existing hover tool (if any) and create a new one
status_plot_unique.tools = [tool for tool in status_plot_unique.tools if not isinstance(tool, HoverTool)]
hover_status = HoverTool(
tooltips=[
("Production Status", '@Production_Status'),
("Percentage", "@Percentage_Status{0.0f}%")
]
)
status_plot_unique.add_tools(hover_status)
status_plot_unique.hbar(
y='Production_Status',
right='Percentage_Status',
source=source_status_renamed,
height=0.6, # Thickness of each bar
color=factor_cmap('Production_Status', palette=colors_palette_status, factors=categories_status),
legend_field='Production_Status' # Add legend field
)
# Set fixed dimensions directly here
status_plot_unique.width = 600
status_plot_unique.height = 600
# HTML title for Product Category
title_text_category = """<span style="font-size: 16px; color: #305496; margin-left: 20px;">
Product Category <b>Distribution</b> based on <b>Unique</b> Top-Level
</span>"""
title_category_plot_unique = Div(text=title_text_category)
# Create figure for Product Category
category_plot_unique = figure(
y_range=FactorRange(*transformed_category['Product Category']),
x_axis_label='Percentage Product Category',
y_axis_label='Product Category',
x_range=(0, 100),
title=None
)
# Remove existing hover tool (if any) and create a new one
category_plot_unique.tools = [tool for tool in category_plot_unique.tools if not isinstance(tool, HoverTool)]
hover_category = HoverTool(
tooltips=[
("Product Category", '@Product_Category'),
("Percentage", "@Percentage_Status{0.0f}%")
]
)
category_plot_unique.add_tools(hover_category)
category_plot_unique.hbar(
y='Product_Category',
right='Percentage_Status',
source=source_category_renamed,
height=0.6, # Thickness of each bar
color=factor_cmap('Product_Category', palette=colors_palette_category, factors=categories_category),
legend_field='Product_Category' # Add legend field
)
# Set fixed dimensions directly here
category_plot_unique.width = 450
category_plot_unique.height = 600
# Set legend position
category_plot_unique.legend.location = 'top_right' # Position the legend in the top right
status_plot_unique.legend.location = 'top_right' # Position the legend in the top right
# Apply customization for consistency with other graphs
status_plot_unique = customize_distribution_plot(status_plot_unique)
category_plot_unique = customize_distribution_plot(category_plot_unique)
# Customize grid lines
status_plot_unique.xgrid.grid_line_color = '#E0E0E0'
status_plot_unique.xgrid.grid_line_dash = [4, 6] # Dash style for x-axis grid lines
status_plot_unique.ygrid.grid_line_color = None # Remove y-axis grid lines
category_plot_unique.xgrid.grid_line_color = '#E0E0E0'
category_plot_unique.xgrid.grid_line_dash = [4, 6] # Dash style for x-axis grid lines
category_plot_unique.ygrid.grid_line_color = None # Remove y-axis grid lines
# Arrange plots and titles in columns
status_layout_unique = column(title_status_plot_unique, status_plot_unique)
category_layout_unique = column(title_category_plot_unique, category_plot_unique)
#return status_plot_unique, category_plot_unique # Update 10/07
return status_layout_unique, category_layout_unique
# Update function for widget changes
def update_plot_percentage_product_category_production_status_UNIQUE(event):
program = program_widget_historic.value # Fetch selected program
# Filter data
filtered_data_status = status_percentage_unique_pivot[status_percentage_unique_pivot['Program'] == program]
if filtered_data_status.empty:
print(f"No data found for the selected program: {program}")
return
# Create updated plots
bokeh_status_unique_plot, bokeh_category_unique_plot = create_percentage_product_category_production_status_UNIQUE(
status_percentage_unique_pivot,
category_percentage_unique_pivot,
program,
colors_palette
)
# Update Bokeh plot panes
bar_plot_pane_status_unique.object = bokeh_status_unique_plot
bar_plot_pane_category_unique.object = bokeh_category_unique_plot
# Create initial plots
bokeh_status_unique_plot, bokeh_category_unique_plot = create_percentage_product_category_production_status_UNIQUE(
status_percentage_unique_pivot,
category_percentage_unique_pivot,
default_program_historic,
colors_palette
)
#######################
# Create initial plots
######################
# Create initial plots --> 2 distinct plots
bokeh_status_unique_plot, bokeh_category_unique_plot = create_percentage_product_category_production_status_UNIQUE(status_percentage_unique_pivot, category_percentage_unique_pivot, default_program_historic, colors_palette)
# Create Bokeh plots panes with fixed dimensions
bar_plot_pane_status_unique = pn.pane.Bokeh(bokeh_status_unique_plot, sizing_mode='fixed', height=600, width=600)
bar_plot_pane_category_unique = pn.pane.Bokeh(bokeh_category_unique_plot, sizing_mode='fixed', height=600, width=450)
####################################################
# Watch the widget and update the plot on change
####################################################
program_widget_historic.param.watch(update_plot_percentage_product_category_production_status_UNIQUE, 'value')
###############################
# Text bellow Chart 4
#############################
# Define text components with fixed widths to match plot widths
text_below_percentage_product_category = pn.pane.HTML(
f"These graphs are based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>% Product Category</b>: Percentage of each Product Categories based on the 'Total Quantity' of each PN.<br>"
"▷ <b>These Graphs includes</b>: All PN since the beginning of the transfer for the selected program included the 'redlist'.<br>",
width=450 # Match the width of bar_plot_pane4 which is 2 grpahs of 225
)
# Define text components with fixed widths to match plot widths
text_below_percentage_production_status = pn.pane.HTML(
f"These graphs are based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>% Production Status</b>: Percentage of each Production Statuses based on the 'Total Quantity' of each PN.<br>"
"▷ <b>These Graphs includes</b>: All PN since the beginning of the transfer for the selected program included the 'redlist'.<br>",
width=620 # Match the width of bar_plot_pane4 which is 2 grpahs of 225
)
####################################################################################
# Text bellow Chart 5 - bar_plot_pane_status_unique & bar_plot_pane_category_unique
#####################################################################################
# Define text components with fixed widths to match plot widths
text_below_percentage_product_category_UNIQUE = pn.pane.HTML(
f"These graphs are based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>% Product Category</b>: Percentage of each Product Categories based on the number of unique PN.<br>"
"▷ <b>These Graphs includes</b>: All PN since the beginning of the transfer for the selected program included the 'redlist'.<br>",
width=450 # Match the width of bar_plot_pane4 which is 2 grpahs of 225
)
# Define text components with fixed widths to match plot widths
text_below_percentage_production_status_UNIQUE = pn.pane.HTML(
f"These graphs are based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>% Production Status</b>: Percentage of each Production Statuses based on the number of unique PN.<br>"
"▷ <b>These Graphs includes</b>: All PN since the beginning of the transfer for the selected program included the 'redlist'.<br>",
width=650 # Match the width of bar_plot_pane4 which is 2 grpahs of 225
)
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
#################################################################################################
# LAYOUT - Combine plots into a vertical Panel layout - Chart 1 to 5
####################################################################################################
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
#Create vertical layouts for each plot and its corresponding text
plot1_layout = pn.Column(
bar_plot_pane1,
pn.Spacer(height=30),
text_below_product_category
)
plot2_layout = pn.Column(
bar_plot_pane2,
pn.Spacer(height=30),
text_below_product_category_total_qty
)
plot3_layout = pn.Column(
bar_plot_pane3,
pn.Spacer(height=30),
text_below_production_status
)
plot_category_percentage = pn.Column(
bar_plot_pane_category_percentage,
pn.Spacer(height=30),
text_below_percentage_product_category
)
plot_status_percentage = pn.Column(
bar_plot_pane_status_percentage,
pn.Spacer(height=30),
text_below_percentage_production_status
)
plot_category_unique = pn.Column(
bar_plot_pane_category_unique,
pn.Spacer(height=30),
text_below_percentage_product_category_UNIQUE
)
plot_status_unique = pn.Column(
bar_plot_pane_status_unique,
pn.Spacer(height=30),
text_below_percentage_production_status_UNIQUE
)
###############################################
# Update 09/16
# Create a vertical divider with custom CSS
###############################################
vertical_divider = pn.pane.HTML(
'<div style="width: 1px; height: 800px; background-color:#D9D9D9;"></div>',
)
vertical_divider2 = pn.pane.HTML(
'<div style="width: 1px; height: 800px; background-color:#D9D9D9;"></div>',
)
###############################################################
# Production Status
##############################################################
# Combine plots and content into columns and rows
distribution_dashboard_production_status = pn.Row(
pn.Column(
pn.pane.HTML("<h2 style='font-size: 18px; color: black; text-align: center; font-weight: bold; padding-left: 15px;'> #1 Based on <u>Unique</u> Part Number in the backlog</h2>"),
pn.Row(plot3_layout, pn.Spacer(width=30), plot_status_unique) # Place plots in the same row
),
pn.Row(pn.Spacer(width=30), vertical_divider, pn.Spacer(width=30)),
pn.Column(
pn.pane.HTML("<h2 style='font-size: 18px; color: black; text-align: center; font-weight: bold; padding-left: 15px;'> #2 Based on <u>Total Quantity</u> of Top-Level in the backlog</h2>"),
plot_status_percentage
)
)
########################
# Product Category
########################
# Combine plots in another horizontal row
distribution_dashboard_product_category = pn.Row(
pn.Column(
pn.pane.HTML("<h2 style='font-size: 18px; color: black; text-align: center; font-weight: bold; padding-left: 15px;'> #1 Based on <u>Unique</u> Part Number in the backlog</h2>"),
pn.Row(plot1_layout, pn.Spacer(width=30), plot_category_unique) # Place plots in the same row
),
pn.Row(pn.Spacer(width=30), vertical_divider2, pn.Spacer(width=30)),
pn.Column(
pn.pane.HTML("<h2 style='font-size: 18px; color: black; text-align: center; font-weight: bold; padding-left: 15px;'> #2 Based on <u>Total Quantity</u> of Top-Level in the backlog</h2>"),
pn.Row(plot_category_percentage, pn.Spacer(width=30), plot2_layout) # Place plots in the same row
)
)
# Update 08/14
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
###############################################################################################
# Create a bar chart graphs representing the % completion Critical Qty and % Completion Total
###############################################################################################
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
# Include the Combined PN {Program} by using Pivot_table_completion_upated_combinedPN instead of Pivot_table_completion
# Filter out rows where '% Completion Total Qty' = 0 from Pivot_table_completion_upated_combinedPN
Pivot_table_completion_upated_combinedPN_filtered = Pivot_table_completion_upated_combinedPN[Pivot_table_completion_upated_combinedPN['% Completion Total Qty'] != 0]
# Create a key numbers pane (initially empty)
key_numbers_pane = pn.pane.DataFrame(pd.DataFrame(), width=700, index=False)
def update_data(event):
# Filter out rows where '% Completion Total Qty' = 0
filtered_data = Pivot_table_completion_upated_combinedPN[Pivot_table_completion_upated_combinedPN['% Completion Total Qty'] != 0]
# Define the target value for 'Pty Indice'
target_value = f'Combined PN {program_widget_historic.value}'
# Filter the DataFrame based on the target value
filtered_data_target_value = filtered_data[filtered_data['Pty Indice'] == target_value]
# Extract the desired columns
key_numbers = filtered_data_target_value[['Pty Indice', 'Qty Shipped', 'Critical Qty', 'Total Quantity', '% Completion Critical Qty', '% Completion Total Qty']]
# Update the key numbers display
key_numbers_pane.object = key_numbers # Update the key numbers pane
# Attach the callback to the widget
program_widget_historic.param.watch(update_data, 'value')
#########################################################################################################################
def customize_completion_plot(bokeh_plot):
"""Apply customizations to the % Completion plot."""
bokeh_plot.xaxis.major_label_text_font_size = '8pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.title.text_font_size = '12pt'
bokeh_plot.title.text_color = "#305496"
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 'vertical'
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#F0F0F0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
# Cap the y-axis at 100%
bokeh_plot.y_range.end = 100
return bokeh_plot
def create_completion_plot(Pivot_table_completion_upated_combinedPN_filtered, default_program_historic):
# Filter data by default program
filtered_data = Pivot_table_completion_upated_combinedPN_filtered[Pivot_table_completion_upated_combinedPN_filtered['Program'] == default_program_historic]
if filtered_data.empty:
print("No data found for the default program.")
return None, None
# Filter rows where either % Completion Critical Qty or % Completion Total Qty is greater than 0
filtered_data = filtered_data[
(filtered_data['% Completion Critical Qty'] > 0) |
(filtered_data['% Completion Total Qty'] > 0)
]
# Melt the DataFrame to long format for plotting
melted_df = filtered_data.melt(
id_vars=['Pty Indice'],
value_vars=['% Completion Critical Qty', '% Completion Total Qty'],
var_name='Completion Type',
value_name='Completion Percentage'
)
# Create a bar chart
completion_plot = melted_df.hvplot.bar(
x='Pty Indice',
y='Completion Percentage',
color='Completion Type',
title="% Completion Critical Qty and % Completion Total Qty per Pty Indice",
xlabel='Pty Indice',
ylabel='% Completion',
cmap='Category20',
legend='top_left',
height=400,
tools=[]
)
# Render and customize the plot
bokeh_completion_plot = hv.render(completion_plot, backend='bokeh')
bokeh_completion_plot = customize_completion_plot(bokeh_completion_plot)
# Remove existing HoverTools (if any) before adding a new one
bokeh_completion_plot.tools = [tool for tool in bokeh_completion_plot.tools if not isinstance(tool, HoverTool)]
# Add HoverTool with custom formatting
hover = HoverTool()
hover.tooltips = [
("Pty Indice", "@Pty_Indice"),
("Completion Type", "@color"),
("Percentage", "@Completion_Percentage%") #"@value{0.1f}%") # Round to 1 decimal
]
bokeh_completion_plot.add_tools(hover)
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
bokeh_completion_plot.tools = [tool for tool in bokeh_completion_plot.tools if not isinstance(tool, WheelZoomTool)]
return bokeh_completion_plot
def update_bar_chart(event):
# Get the selected program from the widget
program = program_widget_historic.value
#print(f"Updating plots for program: {program}")
# Filter data by the selected program
filtered_data = Pivot_table_completion_upated_combinedPN_filtered[Pivot_table_completion_upated_combinedPN_filtered['Program'] == program]
if filtered_data.empty:
print("No data found for the selected program.")
return
# Further filter rows where either % Completion Critical Qty or % Completion Total Qty is greater than 0
filtered_data = filtered_data[
(filtered_data['% Completion Critical Qty'] > 0) |
(filtered_data['% Completion Total Qty'] > 0)
]
# Melt the filtered DataFrame for plotting
melted_df = filtered_data.melt(
id_vars=['Pty Indice'],
value_vars=['% Completion Critical Qty', '% Completion Total Qty'],
var_name='Completion Type',
value_name='Completion Percentage'
)
# Update plots
bokeh_completion_plot = create_completion_plot(filtered_data, program)
# Update the plots in the Panel layout
plot_pane_completion.object = bokeh_completion_plot
# Create initial plot
bokeh_completion_plot = create_completion_plot(Pivot_table_completion_upated_combinedPN_filtered, default_program_historic)
# Convert to Panel/Bokeh
plot_pane_completion = pn.pane.Bokeh(bokeh_completion_plot, sizing_mode='stretch_width')
# Update plot initially - Needed for the sizing_mode='stretch_width' to be set
update_bar_chart(None)
#######################################################
# Watch the widget and update the plot on change
program_widget_historic.param.watch(update_bar_chart, 'value') # moved bellow 10/25
######################################
# Create text bellow graphs
########################################
'''
text_below_completion_plot = (
f"This graph is based on data from |Snapshot| & |CM-Priority|- <b> {file_date} & {Date_CM_Priority}</b>:<br>"
"▷<b>Total Quantity</b>: Is calculated as 'IDD Backlog Qty' + 'Qty Shipped' if 'Remain. crit. Qty' = 0.<br>"
"➥ 'Total Quantity' increases over time as follow-up orders are placed, while the 'Critical Quantity' is defined as part of the project scope.<br>"
"▷ <b>% Completion Critical Qty</b>: Progress based on the defined 'Critical Qty', usually incompassing the DPAS orders.<br>"
"▷ <b>% Completion Total Qty</b>: Progress based on the 'Total Qty' including the potential follow-up orders.<br>"
)
'''
text_below_completion_plot = (
f"<div style='width: 860px;'>"
f"This graph is based on data from |Snapshot| & |CM-Priority|- <b> {file_date} & {Date_CM_Priority}</b>:<br>"
"▷<b>Total Quantity</b>: Is calculated as 'IDD Backlog Qty' + 'Qty Shipped' if 'Remain. crit. Qty' = 0.<br>"
"➥ 'Total Quantity' increases over time as follow-up orders are placed, while the 'Critical Quantity' is defined as part of the project scope.<br>"
"▷ <b>% Completion Critical Qty</b>: Progress based on the defined 'Critical Qty', usually encompassing the DPAS orders.<br>"
"▷ <b>% Completion Total Qty</b>: Progress based on the 'Total Qty' including the potential follow-up orders.<br>"
"</div>"
)
text_above_key_number = (
f"▷ The Data-point <b>'Combined PN'</b>: Represents the full scope of the project for the selected 'Program'.<br>"
"➥ This data-point is a made-up PN representative of the entire scope in term of ' Total Quantity' of the project based on the Priority List.<br>"
"➥ The Canceled orders are filtered-out but the PN 'To be transferred' are still included.<br>"
)
# Arrange text_above_key_number above key_numbers_pane
key_numbers_column = pn.Column(
text_above_key_number,
key_numbers_pane,
sizing_mode='stretch_width'
)
#create short vertical divider
vertical_divider_short = pn.pane.HTML(
'<div style="width: 1px; height: 170px; background-color:#D9D9D9;"></div>',
)
# Arrange text_below_completion_plot and key_numbers_column side-by-side
side_by_side = pn.Row(
text_below_completion_plot,
pn.Spacer(width=100), # Add space between the plot and the text
vertical_divider_short,
pn.Spacer(width=100),
key_numbers_column,
sizing_mode='stretch_width'
)
# Arrange plot_pane_completion on top and side_by_side below it
completion_dashboard = pn.Column(
plot_pane_completion,
pn.Spacer(height=50), # Add space between the plot and the text
side_by_side,
sizing_mode='stretch_width'
)
#08/21
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
#######################################################################################################################
# Backlog Projection --> Quantity of PN to build per Month - Graph combined and Graph PN by PN
#######################################################################################################################
#re-load df_Backlog to erase any potnetial change on the original dataframe
#df_Backlog = pd.read_excel(input_file_formatted, sheet_name='CM-Backlog', index_col=False)
#Rename 'Backlog row Qty' to 'Backlog Qty'
#df_Backlog.rename(columns={'Backlog row Qty': 'Backlog Qty'}, inplace=True)
# 09/19 ---> to be updated with 'Requested Date' & 'Month Requested' <---
#//////////////////////////////////////////////////#//////////////////////////////////////////////////
#Preparation of dataframes backlog_monthly_summary based on df_Backlog
backlog_monthly_summary = df_Backlog.copy()
#Filter relevant column from df_Backlog
backlog_monthly_summary = backlog_monthly_summary[['Priority', 'Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Backlog Qty', 'Marge standard', 'Site', 'Order', 'Invoice name', 'Requested Date', 'Due Date','Actual amount -standard', 'Month', 'Month Requested', 'Product_Category', 'Complexity', 'Program']]
#print('backlog_monthly_summary')
#display(backlog_monthly_summary)
###############################################################
# backlog_monthly_summary dataframe
###############################################################
# Ensure 'Due Date' is in datetime format
backlog_monthly_summary['Due Date'] = pd.to_datetime(backlog_monthly_summary['Due Date'])
backlog_monthly_summary['Requested Date'] = pd.to_datetime(backlog_monthly_summary['Requested Date'])
# Rename columns
backlog_monthly_summary = backlog_monthly_summary.rename(columns={
'Actual amount -standard': 'Sales',
'Marge standard': 'IDD Marge Standard',
'Complexity': 'Average Complexity',
})
# Filter to exclude rows where 'Order' contains 'NC'
backlog_monthly_summary = backlog_monthly_summary[~backlog_monthly_summary['Order'].str.contains('NC')]
#####################################
# Sorting backlog_monthly_summary
######################################
# Function to check if a value is numeric
def is_numeric(val):
try:
int(val)
return True
except ValueError:
return False
# Separate numeric and non-numeric 'Priority' values
backlog_numeric_priority = backlog_monthly_summary[backlog_monthly_summary['Priority'].apply(is_numeric)]
backlog_non_numeric_priority = backlog_monthly_summary[~backlog_monthly_summary['Priority'].apply(is_numeric)]
# Convert 'Priority' values to integers for numeric priorities
backlog_numeric_priority['Priority'] = backlog_numeric_priority['Priority'].astype(int)
# Sort numeric priorities in ascending order
#backlog_numeric_priority = backlog_numeric_priority.sort_values(by='Priority', ascending=True) #Update 08/28
backlog_numeric_priority.sort_values(by=['Priority', 'Pty Indice'], inplace=True)
# Combine the DataFrames, placing numeric priorities first and non-numeric priorities at the end
backlog_monthly_summary_sorted = pd.concat([backlog_numeric_priority, backlog_non_numeric_priority])
# Reset index if needed
backlog_monthly_summary_sorted.reset_index(drop=True, inplace=True)
# Update the original DataFrame
backlog_monthly_summary = backlog_monthly_summary_sorted
#print('backlog_monthly_summary:')
#display(backlog_monthly_summary)
##########################
########################################################################
# Create datafram for Graph 1 by grouping by 'Month' and 'Program'
#########################################################################
#Update 09/19
# 'Month' is related to the 'Due Date' whcih correspond to the modified PO set to build a more sustainable backlog
# 'Month Requested' is related to the 'Requested Date' whcih correspond to the original PO placed by SEDA
###########################
backlog_monthly_summary_combined = backlog_monthly_summary.groupby(['Month', 'Program']).agg({
'Backlog Qty': 'sum',
'Sales': 'sum',
'Pty Indice': lambda x: ', '.join(map(str, x)),
'IDD Top Level': lambda x: ', '.join(map(str, x)),
'SEDA Top Level': lambda x: ', '.join(map(str, x)),
'IDD Marge Standard': 'sum',
'Due Date': 'first', # Keep the 'Invoice date' as the first date in each group
'Average Complexity': 'mean' # Calculate the average complexity
}).reset_index()
# Define a function to format numbers with 1 decimal digit if necessary
def format_complexity(value):
if pd.isna(value): # Handle NaN values
return value
elif value.is_integer():
return int(value) # Return as integer if value is an integer
else:
return round(value, 1) # Round to 1 decimal place otherwise
# Apply the formatting function to 'Complexity' column
backlog_monthly_summary_combined['Average Complexity'] = backlog_monthly_summary_combined['Average Complexity'].apply(format_complexity)
#Create 'Normalized Complexity'
backlog_monthly_summary_combined['Normalized Complexity'] = backlog_monthly_summary_combined['Average Complexity']*backlog_monthly_summary_combined['Backlog Qty']
#print('backlog_monthly_summary_combined')
#display(backlog_monthly_summary_combined)
###################################
# Fill NaN values appropriately
###################################
# Fill numeric columns with 0
Backlog_numeric_cols = backlog_monthly_summary_combined.select_dtypes(include='number').columns
backlog_monthly_summary_combined[Backlog_numeric_cols] = backlog_monthly_summary_combined[Backlog_numeric_cols].fillna(0)
# Fill string columns with ''
Backlog_string_cols = backlog_monthly_summary_combined.select_dtypes(include='object').columns
backlog_monthly_summary_combined[Backlog_string_cols] = backlog_monthly_summary_combined[Backlog_string_cols].fillna('')
# Sort by 'Invoice date' in descending order
backlog_monthly_summary_combined = backlog_monthly_summary_combined.sort_values(by='Due Date', ascending=True)
# Display the updated DataFrame
#print('backlog_monthly_summary_combined:')
#display(backlog_monthly_summary_combined)
##################################################################################################
# Backlog graph 1 - Combined Quantity of PN to build per months
##################################################################################################
##################################################################################################
# Backlog graph 2 - Quantity of PN to build per month for each given Pty Indice
####################################################################################################
##################################################################################################
# Backlog graph 3 - XXXX
####################################################################################################
##################################################################################################
# Backlog graph 4 - Backlog Projection using 'Month Requested' and Requested Date'
####################################################################################################
# Load backlog
df_Backlog_overview = df_Backlog.copy()
### update 08/23
# Custom color palette with alpha transparency
custom_palette_bkg = {
'Backlog Qty': '#cdbedd',
'Sales': '#63BE7B',
'IDD Marge Standard': '#E2EFDA',
'Normalized Complexity': 'rgba(255, 47, 47, 0.7)' # Alpha applied
}
def customize_qty_backlog_plot(bokeh_plot):
""" Apply customizations to the Quantity backlog plot. """
bokeh_plot.xaxis.major_label_text_font_size = '8pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.title.text_font_size = '12pt'
bokeh_plot.title.text_color = "#305496"
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 'vertical'
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#E0E0E0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
return bokeh_plot
def customize_total_quantity_backlog_plot(bokeh_plot):
""" Apply customizations to the Total Quantity plot. """
bokeh_plot.xaxis.major_label_text_font_size = '6pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.title.text_font_size = '12pt'
bokeh_plot.title.text_color = "#305496"
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 'vertical'
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#E0E0E0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
return bokeh_plot
def customize_combined_backlog_plot(bokeh_plot):
""" Apply customizations to the Combined plot. """
bokeh_plot.xaxis.major_label_text_font_size = '8pt'
bokeh_plot.yaxis.major_label_text_font_size = '10pt'
bokeh_plot.title.text_font_size = '12pt'
bokeh_plot.title.text_color = "#305496"
bokeh_plot.xaxis.axis_line_width = 2
bokeh_plot.yaxis.axis_line_width = 2
bokeh_plot.xaxis.major_label_orientation = 'vertical'
bokeh_plot.yaxis.major_label_orientation = 'horizontal'
bokeh_plot.yaxis.axis_label_text_font_size = '10pt'
bokeh_plot.ygrid.grid_line_color = '#F0F0F0'
bokeh_plot.ygrid.grid_line_dash = [4, 6]
bokeh_plot.toolbar.logo = None
# Format the y-axis ticks in thousands with a dollar sign
bokeh_plot.yaxis.formatter =CustomJSTickFormatter(code="""
return '$' + (tick / 1000).toFixed(0) + 'k';
""")
return bokeh_plot
def create_total_quantity_backlog_plot(df_Backlog_overview, default_program_historic):
# Filter data by the default program
filtered_data = df_Backlog_overview[df_Backlog_overview['Program'] == default_program_historic]
if filtered_data.empty:
print("No data found for the default program.")
return None
# Aggregate data: Sum 'Backlog Qty' for each 'Pty Indice'
aggregated_data = filtered_data.groupby('Pty Indice')['Backlog Qty'].sum().reset_index()
# If the program is 'Phase 4-5', sort by 'Priority'
if default_program_historic == 'Phase 4-5':
# Merge the aggregated data with original to retain 'Priority'
aggregated_data = pd.merge(aggregated_data, filtered_data[['Pty Indice', 'Priority']].drop_duplicates(), on='Pty Indice')
# Convert 'Priority' values to integers for sorting
aggregated_data['Priority'] = aggregated_data['Priority'].astype(int)
# Sort numeric priorities in ascending order - Update 08/28
#aggregated_data = aggregated_data.sort_values(by='Priority', ascending=True)
aggregated_data.sort_values(by=['Priority', 'Pty Indice'], inplace=True)
# Define the uniform color
uniform_color = '#cdbedd' # Light blue color
# Create the plot
total_quantity_plot = aggregated_data.hvplot.bar(
x='Pty Indice',
y='Backlog Qty',
title="Total Backlog Qty",
xlabel='Pty Indice',
ylabel='Total Backlog Qty',
#cmap=custom_palette_bkg,
color=uniform_color, # Apply the same color to all bars
legend='top_left',
height=400,
tools=[]
)
return total_quantity_plot
def create_backlog_chart_detailed (backlog_monthly_summary_combined, default_program_historic, df_Backlog_overview):
# Filter data by default program
filtered_data = backlog_monthly_summary_combined[backlog_monthly_summary_combined['Program'] == default_program_historic]
if filtered_data.empty:
print("No data found for the default program.")
return None, None, None
# Melt the DataFrame to include Normalized Complexity
melted_df = filtered_data.melt(id_vars=['Month'], value_vars=['Backlog Qty', 'Sales', 'IDD Marge Standard', 'Normalized Complexity'],
var_name='Quantity Type', value_name='Quantity Value')
# Create plot for 'Backlog Qty' and 'Normalized Complexity'
backlog_qty_plot = melted_df[melted_df['Quantity Type'].isin(['Backlog Qty', 'Normalized Complexity'])].hvplot.bar(
x='Month',
y='Quantity Value',
color='Quantity Type',
title="Monthly Backlog - Backlog Quantity & Normalized Complexity",
xlabel='Month',
ylabel='Backlog Qty & Normalized Complexity',
cmap=custom_palette_bkg,
legend='top_left',
height=400,
bar_width=0.6, # Set bar width - 09/12
tools=[]
)
bokeh_backlog_qty_plot = hv.render(backlog_qty_plot, backend='bokeh')
bokeh_backlog_qty_plot = customize_qty_backlog_plot(bokeh_backlog_qty_plot)
#####################################################
# Remove existing HoverTools (if any) before adding a new one
bokeh_backlog_qty_plot.tools = [tool for tool in bokeh_backlog_qty_plot.tools if not isinstance(tool, HoverTool)]
# Add HoverTool with custom formatting
hover = HoverTool()
hover.tooltips = [
("Month", "@Month"),
("KPI", "@color"),
("Value", "@Quantity_Value")
]
# Add HoverTool to the plot
bokeh_backlog_qty_plot.add_tools(hover)
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
bokeh_backlog_qty_plot.tools = [tool for tool in bokeh_backlog_qty_plot.tools if not isinstance(tool, WheelZoomTool)]
############################################################
# Create combined plot for 'IDD Marge Standard' and 'Sales'
combined_backlog_plot = melted_df[melted_df['Quantity Type'].isin(['IDD Marge Standard', 'Sales'])].hvplot.bar(
x='Month',
y='Quantity Value',
color='Quantity Type',
title="Monthly Backlog - IDD Margin & Total Sales",
xlabel='Month',
ylabel='[K$]',
cmap=custom_palette_bkg,
legend='top_left',
stacked=True, # Stacking bars
height=400,
bar_width=0.6, # Set bar width - 09/12
tools=[]
)
bokeh_combined_backlog_plot = hv.render(combined_backlog_plot, backend='bokeh')
bokeh_combined_backlog_plot = customize_combined_backlog_plot(bokeh_combined_backlog_plot)
#New 08/08
#####################################################
# Remove existing HoverTools (if any) before adding a new one
bokeh_combined_backlog_plot.tools = [tool for tool in bokeh_combined_backlog_plot.tools if not isinstance(tool, HoverTool)]
# Add HoverTool with custom formatting
hover = HoverTool()
hover.tooltips = [
("Month", "@Month"),
("KPI", "@color"),
("Value", "@Quantity_Value{($0,0k)}") # Format values: thousands with 'K' # Quantity_Value with the '_' otherwise that does not work!
]
# Add HoverTool to the plot
bokeh_combined_backlog_plot.add_tools(hover)
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
bokeh_combined_backlog_plot.tools = [tool for tool in bokeh_combined_backlog_plot.tools if not isinstance(tool, WheelZoomTool)]
############################################################
# Create Total Quantity backlog plot
total_quantity_backlog_plot = create_total_quantity_backlog_plot(df_Backlog_overview, default_program_historic)
if total_quantity_backlog_plot:
bokeh_total_quantity_backlog_plot = hv.render(total_quantity_backlog_plot, backend='bokeh')
bokeh_total_quantity_backlog_plot = customize_total_quantity_backlog_plot(bokeh_total_quantity_backlog_plot)
# Remove wheel zoom from active tools if you want it inactive by default - 08/12
bokeh_total_quantity_backlog_plot.tools = [tool for tool in bokeh_total_quantity_backlog_plot.tools if not isinstance(tool, WheelZoomTool)]
else:
bokeh_total_quantity_backlog_plot = None
return bokeh_backlog_qty_plot, bokeh_combined_backlog_plot, bokeh_total_quantity_backlog_plot
def update_backlog_chart_combined(event):
# Get the selected program from the widget
program = program_widget_historic.value
#print(f"Updating plots for program: {program}")
# Filter data by the selected program
filtered_data = backlog_monthly_summary_combined[backlog_monthly_summary_combined['Program'] == program]
if filtered_data.empty:
print("No data found for the selected program.")
return
# Melt the DataFrame
melted_df = filtered_data.melt(id_vars=['Month'], value_vars=['Backlog Qty', 'Sales', 'IDD Marge Standard', 'Normalized Complexity'],
var_name='Quantity Type', value_name='Quantity Value')
# Update plots
bokeh_backlog_qty_plot, bokeh_combined_backlog_plot, bokeh_total_quantity_backlog_plot = create_backlog_chart_detailed(filtered_data, program, df_Backlog_overview)
# Update the plots in the Panel layout
backlog_plot_pane1.object = bokeh_backlog_qty_plot
backlog_plot_pane2.object = bokeh_combined_backlog_plot
backlog_plot_pane3.object = bokeh_total_quantity_backlog_plot
# Create initial bokeh plots
bokeh_backlog_qty_plot, bokeh_combined_backlog_plot, bokeh_total_quantity_backlog_plot = create_backlog_chart_detailed(backlog_monthly_summary_combined, default_program_historic, df_Backlog_overview)
# Convert Bokeh plots to Panel
backlog_plot_pane1 = pn.pane.Bokeh(bokeh_backlog_qty_plot, sizing_mode='stretch_width')
backlog_plot_pane2 = pn.pane.Bokeh(bokeh_combined_backlog_plot, sizing_mode='stretch_width')
backlog_plot_pane3 = pn.pane.Bokeh(bokeh_total_quantity_backlog_plot, sizing_mode='stretch_width')
# Update plot initially - Needed for the sizing_mode='stretch_width' to be set
update_backlog_chart_combined(None)
#######################################################
# Watch the widget and update the plot on change
program_widget_historic.param.watch(update_backlog_chart_combined, 'value')
#//////////////////////////////////////
############################################################################################
# Display the datafram monthly_summary of list of Pty Indice for each Month under Graph 3
#############################################################################################
#///////////////////////////////////////
# Function to remove duplicates in comma-separated strings
def remove_duplicates_from_string(s):
items = s.split(', ')
unique_items = sorted(set(items), key=items.index) # Preserve order
return ', '.join(unique_items)
# Function to filter and sort DataFrame by program and month (for backlog)
def filter_dataframe_monthly_summary_backlog(program):
# Apply the filter based on selected program
filtered_df = backlog_monthly_summary_combined[backlog_monthly_summary_combined['Program'] == program]
# Check if the filtered DataFrame is empty
if filtered_df.empty:
print("No data found for the specified program.") # New check for empty DataFrame
return filtered_df # Return empty DataFrame if no matches found
# Filter columns
filtered_df = filtered_df[['Month', 'Pty Indice', 'Backlog Qty', 'IDD Top Level']]
# Remove duplicates in specified columns
filtered_df['Pty Indice'] = filtered_df['Pty Indice'].apply(remove_duplicates_from_string)
filtered_df['IDD Top Level'] = filtered_df['IDD Top Level'].apply(remove_duplicates_from_string)
# Create a temporary column for sorting by converting 'Month' to datetime
filtered_df['Month_dt'] = pd.to_datetime(filtered_df['Month'], format='%b %y', errors='coerce')
# Check for any invalid dates after conversion
if filtered_df['Month_dt'].isnull().any():
print("Some dates could not be parsed. Please check the 'Month' column for incorrect formats.") # Error handling
return filtered_df # Return DataFrame without sorting
# Sort by the new 'Month_dt' column
filtered_df = filtered_df.sort_values(by='Month_dt') # ascending=False - Do not set by desceding to display older month first
# Reset the index after sorting
filtered_df.reset_index(drop=True, inplace=True)
# Print the DataFrame before deleting the temporary column
#print("Filtered and sorted DataFrame before dropping the temporary column:")
#display(filtered_df) # Displaying the DataFrame for verification
# Drop the 'Month_dt' column
filtered_df = filtered_df.drop(columns=['Month_dt'])
return filtered_df
#####################################################
# Table colored in purple and white every other rows
#######################################################
# Function to apply custom styles to the DataFrame (alternating row colors)
def style_dataframe_purple(df):
def row_styles(row):
# Alternate row colors based on row index
color = '#E4DFEC' if row.name % 2 == 0 else '#ffffff' # Alternate colors
return [f'background-color: {color}'] * len(row) # Apply to all columns
# Apply the style function to the DataFrame rows
styled_df = df.style.apply(row_styles, axis=1)
# Hide the index
styled_df.hide(axis="index") # Hide index
return styled_df
# Function to update DataFrame display with custom styling
def update_dataframe_monthly_summary_backlog(program):
filtered_df = filter_dataframe_monthly_summary_backlog(program)
styled_df = style_dataframe_purple(filtered_df)
styled_html = styled_df.to_html() # New 10/24
# Add CSS for overflow handling directly in the HTML
html_with_overflow = f'<div style="overflow-y: auto; height: 450px;">{styled_html}</div>'
return html_with_overflow
# Initialize the backlog table
monthly_summary_backlog_table = pn.pane.HTML(update_dataframe_monthly_summary_backlog(default_program_historic), width=700)
# Callback function to update the table based on widget value
def update_table_backlog(event):
print(f"Widget value changed to: {event.new}") # Check new value
new_df = filter_dataframe_monthly_summary_backlog(event.new)
# Style the new DataFrame
styled_df = style_dataframe_purple(new_df)
# Convert styled DataFrame to HTML for rendering
styled_html = styled_df.to_html()
html_with_overflow = f'<div style="overflow-y: auto; height: 450px;">{styled_html}</div>'
# Update the object attribute directly
monthly_summary_backlog_table.object = html_with_overflow # Update existing HTML pane
# Attach callback to the widget
program_widget_historic.param.watch(update_table_backlog, 'value')
# Create a backlog high level summary table with 'Backlog Quantity' of PN and 'Sales' related to 'Total Past due backlog',
# 'Total future backlog', 'Total current year remaining backlog'
backlog_highlevelsummary_table = None
######################################
# Create text bellow graphs
########################################
# Convert 'Invoice date' to datetime format
df_Backlog_overview['Due Date'] = pd.to_datetime(df_Backlog_overview['Due Date'])
text_below_graph_backlog_qty_plot = (
f"This graph is based on data from |CM-Backlog|:<br>"
"▷ <b>Backlog Qty</b>: Total quantity of Top-Level related to the selected program in IDD backlog <br>"
"➥ The backlog does not necessarily represent the Master Production Schedule (MPS) as manually entered by the Master Scheduler. <br>"
"▷ <b>Normalized Complexity</b>: Average complexity of the Top-Level in backlog normalized on the quantity of each PN on the period.<br>"
"▷ <b>The complexity is define as</b>: Kit, Subs = 0, Lighplate = 1, Rotottelite = 2, CPA = 3, ISP = 4.<br>"
)
text_below_graph_Marge_Sales_Backlog = (
f"This graph is based on data from |CM-Backlog|:<br>"
"▷ <b>Sales</b>: Sum of the 'Currency turnover ex.VAT' for the PN in backlog during the specified month<br>"
"▷ <b>IDD Marge Standard</b>: Sum of the 'IDD Margin Standard' for the PN in backlog during the specified month.<br>"
"➥ The value is displayed as: Gain (Loss). <br>"
)
text_below_graph_backlog_pty_indice = (
f"This graph is based on data from |CM-Backlog|:<br>"
"▷ <b>Total Backlog Qty </b>: Total quantity of Top-Level related to the selected pty Indice in IDD Backlog.<br>"
)
##############################################
# Combine plots into a vertical Panel layout
###############################################
# Combine the plots and table in the layout
# Create the dashboard layout for backlog overview
Backlogoverview_dashboard = pn.Column(
pn.Row(
# First Row with two columns
pn.Column(
backlog_plot_pane1, # First plot
text_below_graph_backlog_qty_plot # Text below first plot
),
pn.Spacer(width=50), # Spacer between columns
vertical_divider_medium2, # Vertical divider
pn.Spacer(width=50), # Spacer between columns
pn.Column(
backlog_plot_pane2, # Second plot
text_below_graph_Marge_Sales_Backlog # Text below second plot
),
sizing_mode='stretch_width' # Stretch columns to fit width
),
pn.Spacer(height=50), # Spacer before the next row
pn.Row(
# Second Row with another two columns
pn.Column(
backlog_plot_pane3, # Third plot
text_below_graph_backlog_pty_indice # Text below third plot
),
monthly_summary_backlog_table, # Summary table on the right
),
)
#//////////////////////////////////////////////////
#######################################################################################################################
# Fianal Layout of the |Project Overview| tab
#######################################################################################################################
#//////////////////////////////////////////////////
# Define your color
line_color = "#4472C4" # Change this to your desired color
font_top_color = "#4472C4"
subtitle_background_color = "#aee0d9" # "#F2F2F2" #Gray
# Convert end_date_historic to the desired format
formatted_end_date = end_date_historic.strftime("%m/%d/%Y")
# Use the formatted date in your string
Historic_title = f"Transfer Project Overview [{formatted_end_date}]"
Historic_subtitle = "Selection of the program"
Historic_subtitle2 = "Monthly shipments & related Sales"
Historic_subtitle3bis = " Distribution of product category"
Historic_subtitle3 = " Distribution of production status"
Historic_subtitle4 = "Pourcentage Completion of the project"
Historic_subtitle5 = "Backlog Overview"
Historic_subsubtitle1 = "Backlog high level summary"
###########################################
title_section = pn.pane.HTML(f"""
<div style='background-color: {font_top_color}; width: 100%; padding: 10px; box-sizing: border-box;'>
<h1 style='font-size: 24px; color: white; text-align: left; margin: 0;'>{Historic_title}</h1>
</div>
""", sizing_mode='stretch_width')
# Title Layout
title_layout = pn.Column(
title_section,
pn.layout.Divider(margin=(-10, 0, 0, 0)),
pn.Column(
#pn.pane.HTML(f"<h3 style='font-size: 12px; text-align: center; font-weight: normal;'>{Historic_subtitle}</h3>"),
pn.layout.Spacer(height=5),
pn.Row(
program_widget_historic,
sizing_mode='stretch_width'
),
sizing_mode='stretch_width'
),
sizing_mode='stretch_width'
)
# Update 04/10 to conditionnaly display the distribution_dashboard_product_category
# Define Secondary Layout
secondary_layout = pn.Column(
pn.pane.HTML(
f"""
<div style='background-color: {subtitle_background_color};
width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 15px;'>
<h1 style='font-size: 22px; color: white; text-align: left; margin: 0;'>
{Historic_subtitle2}
</h1>
</div>
""",
sizing_mode='stretch_width'
),
pn.Spacer(height=50), # Spacer before plots
combined_plots_history,
pn.Spacer(height=50),
pn.layout.Divider(margin=(0, 0, -10, 0)),
# Percentage Completion of the project section
pn.pane.HTML(
f"""
<div style='background-color: {subtitle_background_color};
width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 15px;'>
<h1 style='font-size: 22px; color: white; text-align: left; margin: 0;'>
{Historic_subtitle4}
</h1>
</div>
""",
sizing_mode='stretch_width'
),
pn.Spacer(height=50), # Spacer before plots
completion_dashboard,
pn.Spacer(height=50),
pn.layout.Divider(margin=(0, 0, -10, 0)),
# Backlog Overview section
pn.pane.HTML(
f"""
<div style='background-color: {subtitle_background_color};
width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 15px;'>
<h1 style='font-size: 22px; color: white; text-align: left; margin: 0;'>
{Historic_subtitle5}
</h1>
</div>
""",
sizing_mode='stretch_width'
),
pn.Spacer(height=50), # Spacer before plots
Backlogoverview_dashboard,
pn.Spacer(height=50), # Spacer before plots
pn.layout.Divider(margin=(0, 0, -10, 0)),
# Distribution of product category section
pn.pane.HTML(
f"""
<div style='background-color: {subtitle_background_color};
width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 15px;'>
<h1 style='font-size: 22px; color: white; text-align: left; margin: 0;'>
{Historic_subtitle3bis}
</h1>
</div>
""",
sizing_mode='stretch_width'
),
pn.Spacer(height=50), # Spacer before plots
distribution_dashboard_product_category,
pn.Spacer(height=50), # Spacer before plots
pn.layout.Divider(margin=(0, 0, -10, 0)),
# Distribution of production status section
pn.pane.HTML(
f"""
<div style='background-color: {subtitle_background_color};
width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 15px;'>
<h1 style='font-size: 22px; color: white; text-align: left; margin: 0;'>
{Historic_subtitle3}
</h1>
</div>
""",
sizing_mode='stretch_width'
),
pn.Spacer(height=50), # Spacer before plots
distribution_dashboard_production_status,
pn.Spacer(height=50), # Spacer before plots
)
# Combine Title, Primary, and Secondary Layouts
historic_tab = pn.Column(
title_layout,
pn.layout.Divider(margin=(0, 0, -10, 0)), # Add some space between primary and secondary layouts if needed
secondary_layout,
pn.Spacer(height=50), # Spacer before plots
pn.layout.Divider(margin=(0, 0, -10, 0)), # Add some space between primary and secondary layouts if needed
sizing_mode='stretch_width' # Ensure the final layout stretches to fill available space
)
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# |Priority List|
##############################################################################################################################
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#Load df_Priority as it has been filtered previously on the code
df_Priority_table = pd.read_excel(input_file_formatted, sheet_name='CM-Priority', index_col=False)
#----------------------------------------------------------
# 02/11 - Change 'Phase 4' or 'Phase 5' with 'Phase 4-5'
#----------------------------------------------------------
# For df_Priority
if 'Program' in df_Priority_table.columns and 'Pty Indice' in df_Priority_table.columns:
mask = (
df_Priority_table['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Priority_table['Pty Indice'].str.contains('Phase5', na=False)
)
df_Priority_table.loc[mask, 'Program'] = 'Phase 4-5'
######################
# Create Datafram
######################
#Display a simplified CM-Priorty to have access to the IDD Top-Level and SEDA Top-Level associated with the Pty Indice
List_priority = df_Priority_table[['Priority', 'Pty Indice', 'IDD Top Level', 'SEDA Top Level', 'Description', 'Critical Qty', 'Production Status', 'Program']]
# Include 'Product Category' within List_priority
#List_priority['Product Category'] = List_priority['Description'].apply(determine_category)
# Replace 0 with 'Not yet assigned' in ['IDD Top Level']
List_priority['IDD Top Level'] = List_priority['IDD Top Level'].replace(0, 'Not yet assigned')
#################################################################################################################
# Widgets initialization
################################################################################################################
# Defaults program
default_program_List = 'Phase 4-5'
# Widgets initialization
unique_programs_List = df_Priority_table['Program'].dropna().unique().tolist()
default_program_List = unique_programs_List[0] # Use the first available program if the default is not in the list
program_widget_List = pn.widgets.Select(name='Select Program', options=unique_programs_List, value=default_program_List)
#-------------
# 03/03
def filter_dataframe(df, program, idd_filter, seda_filter):
"""
Filter the DataFrame based on Program, IDD Top Level, and SEDA Top Level.
"""
filtered_df = df[df['Program'] == program]
if 'IDD Top Level' in filtered_df.columns and idd_filter:
filtered_df = filtered_df[filtered_df['IDD Top Level'].str.contains(idd_filter, case=False, na=False)]
if 'SEDA Top Level' in filtered_df.columns and seda_filter:
filtered_df = filtered_df[filtered_df['SEDA Top Level'].str.contains(seda_filter, case=False, na=False)]
return filtered_df
#-------------------------------------------------------------------
# Define filtering widgets for 'IDD Top Level' and 'SEDA Top Level' (Priority List)
#-------------------------------------------------------------------
label_idd_top_level_priority = pn.pane.HTML('<b style="color:#2B70B3;">IDD Top Level Filter</b>')
label_seda_top_level_priority = pn.pane.HTML('<b style="color:#2B70B3;">SEDA Top Level Filter</b>')
filters_top_level_priority = {
'IDD Top Level': pn.widgets.TextInput(name='', placeholder='Enter IDD Top Level'),
'SEDA Top Level': pn.widgets.TextInput(name='', placeholder='Enter SEDA Top Level'),
}
# Create buttons for applying filters (Priority List)
filters_top_level_button_priority = pn.widgets.Button(name='Apply Filters', button_type='primary')
reset_button_priority = pn.widgets.Button(name='Reset Filters', button_type='danger')
# Set default value to None for all filter widgets
for widget in filters_top_level_priority.values():
widget.value = ''
# Create the layout with labels, filter widgets, and buttons (Priority List)
filter_widgets_top_level_priority = pn.Row(
pn.Column(label_idd_top_level_priority, filters_top_level_priority['IDD Top Level']),
pn.Column(label_seda_top_level_priority, filters_top_level_priority['SEDA Top Level']),
pn.Column(
pn.Spacer(height=25), # Spacer before the buttons
pn.Row(filters_top_level_button_priority, reset_button_priority)
)
)
#--- 05/16 update
# Define the filter_dataframe function
def filter_dataframe(df, program, idd_filter, seda_filter):
"""
Filter the DataFrame based on Program, IDD Top Level, and SEDA Top Level.
"""
filtered_df = df[df['Program'] == program]
if idd_filter:
filtered_df = filtered_df[filtered_df['IDD Top Level'].str.contains(idd_filter, case=False, na=False)]
if seda_filter:
filtered_df = filtered_df[filtered_df['SEDA Top Level'].str.contains(seda_filter, case=False, na=False)]
return filtered_df
# Define callback function for the Apply Filters button (Priority List)
def on_top_level_filter_button_click_priority(event):
idd_filter = filters_top_level_priority['IDD Top Level'].value
seda_filter = filters_top_level_priority['SEDA Top Level'].value
selected_program = program_widget_List.value
# Filter the DataFrame using the filter_dataframe function
filtered_df = filter_dataframe(List_priority, selected_program, idd_filter, seda_filter)
# Update the Program widget options based on the filtered DataFrame
unique_programs = filtered_df['Program'].dropna().unique().tolist()
if unique_programs:
program_widget_List.options = sorted(unique_programs)
# Ensure the selected program is still valid
if selected_program not in unique_programs:
program_widget_List.value = unique_programs[0]
else:
program_widget_List.options = [selected_program]
filtered_df = List_priority[List_priority['Program'] == selected_program]
# Update the table
priority_table_pane.object = format_priority_with_colors(filtered_df)
# Define callback function for the Reset Filters button (Priority List)
def on_reset_button_click_priority(event):
# Reset the filter widgets to their default values
filters_top_level_priority['IDD Top Level'].value = ''
filters_top_level_priority['SEDA Top Level'].value = ''
# Reset the Program widget to its default value
program_widget_List.options = sorted(df_Priority_table['Program'].dropna().unique().tolist())
program_widget_List.value = default_program_List
# Reset the table to the original data for the default program
filtered_df = List_priority[List_priority['Program'] == default_program_List]
priority_table_pane.object = format_priority_with_colors(filtered_df)
# Link the buttons to their respective callbacks
filters_top_level_button_priority.on_click(on_top_level_filter_button_click_priority)
reset_button_priority.on_click(on_reset_button_click_priority)
#-------------------------------------------------------------------
#--- 05/16 END update
###################################################################
# Function to update the DataFrame based on the selected program
####################################################################
#######################
# Define color mappings
#########################
color_mapping_production_status = {
'Industrialized': '#D8E4BC', # Light Gren
'FTB WIP': '#DAEEF3', # Light Blue
'Proto WIP': '#DAEEF3', # Light Blue
'Completed': '#75B44A', # # Gray fill '#F2F2F2' or Dark green '##75B44A'
'To be transferred': '#F2DCDB', # Light red
'Officially transferred':'#FF7A5B',
'Canceled': '#F35757' # Dark red
}
font_mapping_production_status = {
'Industrialized': '#375623', # Dark green
'FTB WIP': '#0070C0', # Dark Blue
'Proto WIP': '#0070C0', # Dark Blue
'Completed': '#375623', # Dark green
'To be transferred': '#C00000', # Dark red
'Officially transferred':'#C00000',
'Canceled': '#C00000' # Dark red
}
# Define border and alignment styles
light_gray_border = 'border: 1px solid #D3D3D3;'
centered_text = 'text-align: center;'
def apply_color_and_bold(row):
"""
Apply color and bold formatting to a row based on 'Production Status' and 'Pty Indice' values.
"""
styles = [''] * len(row)
font_colors = [''] * len(row)
production_status = row.get('Production Status', '')
pty_indice = row.get('Pty Indice', '')
# Determine the background color based on 'Production Status'
if production_status in color_mapping_production_status:
color = color_mapping_production_status[production_status]
else:
color = '#FFFFFF' # Default to white if status is not in the mapping
# Determine the font color based on 'Production Status'
if production_status in font_mapping_production_status:
font_color = f'color: {font_mapping_production_status[production_status]};'
else:
font_color = '' # Default to no color if status is not in the mapping
# Apply background color and border to each cell
for i in range(len(row)):
styles[i] = f'background-color: {color}; {light_gray_border}; {centered_text}'
if font_colors[i]: # Apply font color if it's set
styles[i] += f'; {font_colors[i]}'
# Add font color specifically to 'Production Status' and 'Pty Indice' cells
if 'Production Status' in row.index:
production_status_index = row.index.get_loc('Production Status')
styles[production_status_index] += f'; {font_color}; font-weight: bold;'
if 'Pty Indice' in row.index:
pty_indice_index = row.index.get_loc('Pty Indice')
styles[pty_indice_index] += f'; {font_color}; font-weight: bold;'
return styles
def format_priority_with_colors(df):
"""
Format DataFrame with colors and alignment.
"""
# Define header styling to center the text
header_style = {
'selector': 'thead th',
'props': [('text-align', 'center')]
}
# Apply color formatting and header styling
return df.style \
.apply(lambda row: apply_color_and_bold(row), axis=1) \
.set_table_styles([header_style]) \
.hide(axis="index") \
.hide(subset=['Program'], axis="columns") # Use .hide() with subset to hide the 'Program' column
##################
#Create panel pane
#################
# Function to update the DataFrame based on the selected program - 03/04
def update_priority_table(event):
selected_program = event.new # Use event.new to get the new value
filtered_df = List_priority[List_priority['Program'] == selected_program]
# Format the DataFrame only once
priority_table_pane.object = format_priority_with_colors(filtered_df)
# Initial display
initial_filtered_df = List_priority[List_priority['Program'] == default_program_List]
priority_table_pane = pn.pane.DataFrame(format_priority_with_colors(initial_filtered_df), width=1000, index=False)
# Attach the callback to the program_widget_List
program_widget_List.param.watch(update_priority_table, 'value')
#################################
# Layout
#################################
Priority_title = "Priority List"
text_above_Priority = (
f"This table is based on data from |CM-Priority| - <b>{Date_CM_Priority}</b>:<br>"
"▷ <b>Priority List</b>: This table represents the total scope of the Transfer Project for the selected 'Program'.<br>"
"➥ It includes all PNs related to the project, regardless of whether they still have an existing IDD Backlog or if the 'Critical Quantity' defined as part of the transfer project has been reached.<br>"
"➥ Some PNs may not yet have an assigned IDD PN under 'IDD Top-Level'. In such cases, the BOM does not exist, and the given PN won't be present in the |Snapshot| table.<br>"
"➥ The color formatting is based on [<b>'Production Status'</b>].<br>"
)
# Create the title section for the Priority Tab
priority_title_section = pn.pane.HTML(f"""
<div style='background-color: {font_top_color}; width: 100%; padding: 10px; box-sizing: border-box;'>
<h1 style='font-size: 24px; color: white; text-align: left; margin: 0;'>{Priority_title}</h1>
</div>
""", sizing_mode='stretch_width')
# Create the layout for Priority Tab
priority_tab = pn.Column(
priority_title_section,
pn.layout.Divider(margin=(-10, 0, 0, 0)),
pn.Row(program_widget_List, sizing_mode='stretch_width'),
filter_widgets_top_level_priority,
pn.Spacer(height=25),
pn.layout.Divider(margin=(-10, 0, 0, 0)),
pn.Spacer(height=5),
text_above_Priority,
pn.Spacer(height=5),
priority_table_pane,
sizing_mode='stretch_width'
)
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# |Snapshot|
##############################################################################################################################
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#Load df_Snapshot_table as it has been filtered previously on the code
df_Snapshot_table = pd.read_excel(input_file_formatted, sheet_name='Snapshot', index_col=False)
#----------------------------------------------------------
# 02/11 - Change 'Phase 4' or 'Phase 5' with 'Phase 4-5'
#----------------------------------------------------------
# For df_Snapshot (new addition)
if 'Program' in df_Snapshot_table.columns and 'Pty Indice' in df_Snapshot_table.columns:
mask = (
df_Snapshot_table['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Snapshot_table['Pty Indice'].str.contains('Phase5', na=False)
)
df_Snapshot_table.loc[mask, 'Program'] = 'Phase 4-5'
#----------------------------------------------------------
#Map 'Total Qty' from Pivot_table_completion['Total Quantity'] on 'Pty Indice'
# Create the mapping from 'Pty Indice' to 'Total Quantity'
mapping_total_qty = Pivot_table_completion.set_index('Pty Indice')['Total Quantity']
# Apply the mapping to df_Snapshot based on 'Pty Indice'
df_Snapshot_table['Total Quantity'] = df_Snapshot_table['Pty Indice'].map(mapping_total_qty)
#If df_Snapshot['Total Quantity'] = NaN it means that the PN is not in Pibot_table_completion meaning that IDD never shipped it --> Replace NaN with 'Critical Qty' or 'IDD Backlog Qty' watherver is the biggest
# Replace NaN values in 'Total Quantity' with the maximum of 'Critical Qty' or 'IDD Backlog Qty'
df_Snapshot_table['Total Quantity'] = df_Snapshot_table.apply(
lambda row: max(row['Critical Qty'], row['IDD Backlog Qty']) if pd.isna(row['Total Quantity']) else row['Total Quantity'],
axis=1
)
# Filter-out some column for better visibility - 03/03
df_Snapshot_table = df_Snapshot_table.drop(columns=[
'Engineering Cost', 'Start date target', 'IDD Marge Standard (unit)',
'IDD Current Margin (%)', 'IDD Production Cost (unit)', 'Critical Qty', 'IDD Expected ROI (Total)', 'IDD AVG realized sales price [USD]',
'Total WO Count', 'Max Expected Time (full ASSY)[hour]',
'Gap Actual vs Standard [USD]', 'IDD Corrected Cost [USD]',
'IDD Corrected Margin Standard (unit) [USD]', 'IDD AVG realized Margin Standard [USD]', 'Total Quantity', 'Avg Actual Time (full ASSY)[hour]', 'Max Standard Deviation [hour]'
])
# Rename 'Max Standard Deviation [hour]' to 'Standard Deviation [hour]'
#df_Snapshot_table = df_Snapshot_table.rename(columns={'Max Standard Deviation [hour]': 'Standard Deviation [hour]'})
#################################################################################################################
# Widgets initialization
################################################################################################################
# Defaults program
default_program_snapshot = 'Phase 4-5'
# Widgets initialization
unique_programs_snapshot = df_Priority_table['Program'].dropna().unique().tolist()
default_program_snapshot = unique_programs_List[0] # Use the first available program if the default is not in the snapshot
program_widget_snapshot = pn.widgets.Select(name='Select Program', options=unique_programs_snapshot, value=default_program_snapshot)
##---------
# 03/03
#-------------------------------------------------------------------
# Define filtering widgets for 'IDD Top Level' and 'SEDA Top Level' (Snapshot)
#-------------------------------------------------------------------
label_idd_top_level_snapshot = pn.pane.HTML('<b style="color:#2B70B3;">IDD Top Level Filter</b>')
label_seda_top_level_snapshot = pn.pane.HTML('<b style="color:#2B70B3;">SEDA Top Level Filter</b>')
filters_top_level_snapshot = {
'IDD Top Level': pn.widgets.TextInput(name='', placeholder='Enter IDD Top Level'),
'SEDA Top Level': pn.widgets.TextInput(name='', placeholder='Enter SEDA Top Level'),
}
# Create buttons for applying filters (Snapshot)
filters_top_level_button_snapshot = pn.widgets.Button(name='Apply Filters', button_type='primary')
reset_button_snapshot = pn.widgets.Button(name='Reset Filters', button_type='danger')
# Set default value to None for all filter widgets
for widget in filters_top_level_snapshot.values():
widget.value = ''
# Create the layout with labels, filter widgets, and buttons (Snapshot)
filter_widgets_top_level_snapshot = pn.Row(
pn.Column(label_idd_top_level_snapshot, filters_top_level_snapshot['IDD Top Level']),
pn.Column(label_seda_top_level_snapshot, filters_top_level_snapshot['SEDA Top Level']),
pn.Column(
pn.Spacer(height=25), # Spacer before the buttons
pn.Row(filters_top_level_button_snapshot, reset_button_snapshot)
)
)
# Define callback function for the Apply Filters button (Snapshot)
def on_top_level_filter_button_click_snapshot(event):
idd_filter = filters_top_level_snapshot['IDD Top Level'].value
seda_filter = filters_top_level_snapshot['SEDA Top Level'].value
# Filter the DataFrame based on IDD Top Level and SEDA Top Level
filtered_df = df_Snapshot_table.copy()
if idd_filter:
filtered_df = filtered_df[filtered_df['IDD Top Level'].str.contains(idd_filter, case=False, na=False)]
if seda_filter:
filtered_df = filtered_df[filtered_df['SEDA Top Level'].str.contains(seda_filter, case=False, na=False)]
# Update the Program widget options based on the filtered DataFrame
unique_programs = filtered_df['Program'].dropna().unique().tolist()
program_widget_snapshot.options = sorted(unique_programs)
# Set the Program widget value to the first available program (if any)
if unique_programs:
program_widget_snapshot.value = unique_programs[0]
# Update the table
snapshot_table_pane.object = format_snapshot_with_colors(filtered_df)
# Define callback function for the Reset Filters button (Snapshot)
def on_reset_button_click_snapshot(event):
# Reset the filter widgets to their default values
filters_top_level_snapshot['IDD Top Level'].value = ''
filters_top_level_snapshot['SEDA Top Level'].value = ''
# Reset the Program widget to its default value
program_widget_snapshot.options = sorted(df_Snapshot_table['Program'].dropna().unique().tolist())
program_widget_snapshot.value = default_program_snapshot
# Reset the table to the original data
snapshot_table_pane.object = format_snapshot_with_colors(df_Snapshot_table)
# Link the buttons to their respective callbacks
filters_top_level_button_snapshot.on_click(on_top_level_filter_button_click_snapshot)
reset_button_snapshot.on_click(on_reset_button_click_snapshot)
#-------------------
############################
# Formatting snapshot table
###########################
# Replace NaN with 0
df_Snapshot_table = df_Snapshot_table.fillna(0)
#print(df_Snapshot_table[['Max Expected Time (full ASSY)[hour]', 'Avg Actual Time (full ASSY)[hour]']].dtypes)
# Formatting columns
df_Snapshot_table['Shipped'] = df_Snapshot_table['Shipped'].astype(int)
df_Snapshot_table['Remain. crit. Qty'] = df_Snapshot_table['Remain. crit. Qty'].round().astype(int)
# df_Snapshot_table['IDD Marge Standard (unit)'] = df_Snapshot_table['IDD Marge Standard (unit)'].map('${:,.1f}'.format) # Dropped column
df_Snapshot_table['IDD Sale Price'] = df_Snapshot_table['IDD Sale Price'].map('${:,.1f}'.format)
# df_Snapshot_table['IDD Production Cost (unit)'] = df_Snapshot_table['IDD Production Cost (unit)'].map('${:,.1f}'.format) # Dropped column
# df_Snapshot_table['Critical Qty'] = df_Snapshot_table['Critical Qty'].astype(int) # Dropped column
df_Snapshot_table['Qty WIP'] = df_Snapshot_table['Qty WIP'].astype(int)
#df_Snapshot_table['Total Quantity'] = df_Snapshot_table['Total Quantity'].astype(int)
# df_Snapshot_table['Total WO Count'] = df_Snapshot_table['Total WO Count'].astype(int) # Dropped column
# df_Snapshot_table['Max Expected Time (full ASSY)[hour]'] = df_Snapshot_table['Max Expected Time (full ASSY)[hour]'].apply(lambda x: 'No Data' if x == 0 else '{:.2f}'.format(x)) # Dropped column
#df_Snapshot_table['Avg Actual Time (full ASSY)[hour]'] = df_Snapshot_table['Avg Actual Time (full ASSY)[hour]'].apply(lambda x: 'No Data' if x == 0 else '{:.2f}'.format(x))
# df_Snapshot_table['Max Standard Deviation [hour]'] = df_Snapshot_table['Max Standard Deviation [hour]'].apply(lambda x: 'No Data' if x == 0 else '{:.2f}'.format(x)) # Dropped column
#df_Snapshot_table['Standard Deviation [hour]'] = df_Snapshot_table['Standard Deviation [hour]'].apply(lambda x: 'No Data' if x == 0 else '{:.2f}'.format(x)) # Dropped column
# Column name update in df_Snapshot
# Remove the percentage sign and convert to numeric
df_Snapshot_table['Actual vs Standard time [%]'] = pd.to_numeric(df_Snapshot_table['Actual vs Standard time [%]'].str.rstrip('%'), errors='coerce')
# Define a function to format the values or replace NaN with 'N/A'
def format_percentage(value):
if pd.isna(value):
return 'N/A'
return '{:.0f}%'.format(value)
# Apply the function to the column
df_Snapshot_table['Actual vs Standard time [%]'] = df_Snapshot_table['Actual vs Standard time [%]'].apply(format_percentage)
# Replace 0 with ''
#df_Snapshot_table['Start date target'] = df_Snapshot_table['Start date target'].replace(0, '')
###################################################################
# Function to update the DataFrame based on the selected program
##################################################################
#######################
# Define color mappings - 04/16
#########################
color_mapping_production_status = {
'Industrialized': '#cff2c8', # Light Gren
'FTB WIP': '#DAEEF3', # Light Blue
'Proto WIP': '#DAEEF3', # Light Blue
'Completed': '#F2F2F2', # GRAY
'To be transferred': '#F2DCDB', # Light red
'Officially transferred':'#FF7A5B',
'Canceled': '#FF7A5B' # Dark red
}
color_mapping_top_level_status = {
'Clear-to-Build': '#C6EFCE', # Light Green fill for 'Clear-to-Build'
'Short': '#FFC7CE', # Light Red fill for 'Short'
'Completed - No Backlog': '#F2F2F2', # GRAY
'No data': '#FFFFFF', # WHITE
'Completed - Still some Backlog': '#FEFCA6', # Yellow
'Transferred': '#FF7A5B' # Dark red
}
font_mapping_top_level_status = {
'Clear-to-Build': '#4D7731', # Dark Green font for 'Clear-to-Build'
'Short': '#C00000' # Dark Red font for 'Short'
}
font_mapping_production_status = {
'Industrialized': '#375623', # Dark green
'FTB WIP': '#0070C0', # Dark Blue
'Proto WIP': '#0070C0', # Dark Blue
'Completed': '#375623', # Dark green
'To be transferred': '#C00000', # Dark red
'Officially transferred':'#C00000',
'Canceled': '#C00000' # Dark red
}
def apply_color(row):
# Initialize color list with default (empty) colors
colors = [''] * len(row)
font_colors = [''] * len(row)
# Apply color based on 'Top-Level Status'
top_level_status = row.get('Top-Level Status', '')
if top_level_status in color_mapping_top_level_status:
colors[row.index.get_loc('Top-Level Status')] = color_mapping_top_level_status[top_level_status]
if top_level_status in font_mapping_top_level_status:
font_colors[row.index.get_loc('Top-Level Status')] = f'color: {font_mapping_top_level_status[top_level_status]};'
# Apply color for the rest of the row based on 'Production Status'
production_status = row.get('Production Status', '')
if production_status in color_mapping_production_status:
color = color_mapping_production_status[production_status]
for idx, value in enumerate(row):
if row.index[idx] != 'Top-Level Status':
colors[idx] = color
# Apply font color for 'Pty Indice' and 'Production Status' based on 'Production Status'
if production_status in font_mapping_production_status:
font_color = f'color: {font_mapping_production_status[production_status]};'
if 'Production Status' in row.index:
production_status_index = row.index.get_loc('Production Status')
font_colors[production_status_index] = font_color
if 'Pty Indice' in row.index:
pty_indice_index = row.index.get_loc('Pty Indice')
font_colors[pty_indice_index] = font_color
# Apply border, background color, and center text alignment to each cell
cell_styles = [f'background-color: {color}; {light_gray_border}; {centered_text}' for color in colors]
# Add font color to the cells as needed
for idx, font_color in enumerate(font_colors):
if font_color:
cell_styles[idx] += f'; {font_color}'
# Add bold formatting to the 'Production Status' and 'Pty Indice' cells
if 'Production Status' in row.index:
production_status_index = row.index.get_loc('Production Status')
cell_styles[production_status_index] += 'font-weight: bold;'
if 'Pty Indice' in row.index:
pty_indice_index = row.index.get_loc('Pty Indice')
cell_styles[pty_indice_index] += 'font-weight: bold;'
return cell_styles
# Apply color formatting and header centered - 03/04
def format_snapshot_with_colors(df):
"""
Format DataFrame with colors and alignment, and hide the 'Program' column.
"""
# Define header styling
header_style = {
'selector': 'thead th',
'props': [('text-align', 'center')]
}
# Apply color formatting and header styling
return df.style \
.apply(apply_color, axis=1) \
.set_table_styles([header_style]) \
.hide(axis="index") \
.hide(subset=['Program'], axis="columns") # Use .hide() with subset to hide the 'Program' column
#------------------------------------------------
# Reorder snapshot table - 03/04
#------------------------------------------------
# Define the desired column order
desired_columns_snapshot = [
'Program', 'Top-Level Status', 'Pty Indice', 'Priority', 'IDD Top Level', 'SEDA Top Level',
'Shipped', 'Remain. crit. Qty', 'IDD Backlog Qty', 'Qty clear to build', 'Qty WIP',
'Description', 'Production Status', 'IDD Sale Price',
'Actual vs Standard time [%]', 'Deviation vs Actual [%]',
'IDD Corrected Margin [%]', 'IDD AVG realized Margin [%]'
]
# Reorder the columns in df_Snapshot_table
df_Snapshot_table = df_Snapshot_table[desired_columns_snapshot]
##################
#Create panel pane
#################
# Update function
def update_snapshot_table(event):
selected_program = program_widget_snapshot.value
df_filtered = df_Snapshot_table[df_Snapshot_table['Program'] == selected_program]
# Format the DataFrame
styled_df_snapshot = format_snapshot_with_colors(df_filtered)
snapshot_table_pane.object = styled_df_snapshot
# Create the initial styled DataFrame pane
df_initial_filtered = df_Snapshot_table[df_Snapshot_table['Program'] == default_program_snapshot].drop(columns=['Priority', 'Description'])
styled_df_snapshot = format_snapshot_with_colors(df_initial_filtered)
snapshot_table_pane = pn.pane.DataFrame(styled_df_snapshot)
# Attach the callback to the program_widget_List
program_widget_snapshot.param.watch(update_snapshot_table, 'value')
#################################
# Layout
##################################
Snapshot_title = f"Snapshot [{formatted_date}]"
text_above_snapshot = (
f"This table is based on data from |Snapshot| - <b>{file_date}</b>:<br>"
"▷ <b>Snapshot table</b>: This table represents the remaining scope of the Transfer Project for the selected 'Program'.<br>"
"➥ It includes all PNs that have an existing IDD Backlog or for which the 'Critical Quantity', defined as part of the transfer project, has not yet been reached. This applies even if the PN is not currently listed in the IDD Backlog.<br>"
"➥ Some PNs may not yet have an assigned IDD PN under 'IDD Top-Level'. In such cases, the BOM does not exist, and the given PN won't be present in this table.<br>"
"➥ The color formatting is based on ['Top-Level Status'] & ['Production Status'].<br>"
)
# Create the title section for the Priority Tab
Snapshot_title_section = pn.pane.HTML(f"""
<div style='background-color: {font_top_color}; width: 100%; padding: 10px; box-sizing: border-box;'>
<h1 style='font-size: 24px; color: white; text-align: left; margin: 0;'>{Snapshot_title}</h1>
</div>
""", sizing_mode='stretch_width')
# Create the layout for Priority Tab
Snapshot_tab = pn.Column(
Snapshot_title_section,
pn.layout.Divider(margin=(-10, 0, 0, 0)),
pn.Row(program_widget_snapshot, sizing_mode='stretch_width'),
filter_widgets_top_level_snapshot,
pn.Spacer(height=25),
pn.layout.Divider(margin=(-10, 0, 0, 0)),
pn.Spacer(height=5),
text_above_snapshot,
pn.Spacer(height=5),
snapshot_table_pane,
sizing_mode='stretch_width'
)
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# |Cover Dashboard|
##############################################################################################################################
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Loading dataframe used for the |Cover Dashboard|
####################################################
# Create a dictionnay to assigned a 'Product Category' to a 'Pty Indice' based on the function
def determine_category(description):
if not isinstance(description, str):
return 'Others'
if description == 'Rototellite':
return 'Rototellite'
elif 'Indicator' in description or 'CPA' in description:
return 'CPA'
elif 'Lightplate' in description:
return 'Lightplate'
elif 'ISP' in description or 'Keyboard' in description:
return 'ISP'
elif 'Module' in description:
return 'CPA'
elif 'optics' in description:
return 'Fiber Optics'
else:
return 'Others'
################################################################################################
# Sales and shipement progress using panel indicators 'Number', 'Progress' and 'Trend'
################################################################################################
df_Historic_dashboard = pd.read_excel(input_file_formatted, sheet_name='CM-Historic', index_col=False)
df_Priority_dashboard = pd.read_excel(input_file_formatted, sheet_name='CM-Priority', index_col=False)
df_Backlog_dashboard = df_Backlog.copy()
#----------------------------------------------------------
# 02/11 - Change 'Phase 4' or 'Phase 5' with 'Phase 4-5'
#----------------------------------------------------------
# For df_Priority
if 'Program' in df_Priority_dashboard.columns and 'Pty Indice' in df_Priority_dashboard.columns:
mask = (
df_Priority_dashboard['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Priority_dashboard['Pty Indice'].str.contains('Phase5', na=False)
)
df_Priority_dashboard.loc[mask, 'Program'] = 'Phase 4-5'
# For df_Historic
if 'Program' in df_Historic_dashboard.columns and 'Pty Indice' in df_Historic_dashboard.columns:
mask = (
df_Historic_dashboard['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Historic_dashboard['Pty Indice'].str.contains('Phase5', na=False)
)
df_Historic_dashboard.loc[mask, 'Program'] = 'Phase 4-5'
#----------------------------------------------------------
#------------------------------------------------------------------------------------------
# 03/19 - Only keep 'Program' = 'Phase 4-5', 'EMBRAER', SIKORSKY', '1stTB' BUT NOT '2ndTB'
#------------------------------------------------------------------------------------------
# Define the allowed values for the 'Program' column
allowed_programs = ['Phase 4-5', 'EMBRAER', 'SIKORSKY', 'COMAC', '1stTB']
# Apply filtering to each DataFrame
df_Priority_dashboard = df_Priority_dashboard[df_Priority_dashboard['Program'].isin(allowed_programs)]
df_Historic_dashboard = df_Historic_dashboard[df_Historic_dashboard['Program'].isin(allowed_programs)]
#------------------------------------------------------------------------------------------
# Create 'Product_Category' column based on the 'Description' in order to apply the filter if needed
# df_Historic_dashboard already contain ['Product Category']
#df_Backlog_dashboard contain Product_Category
df_Priority_dashboard['Product Category'] = df_Priority_dashboard['Description'].apply(determine_category)
########################################################################################################################################################
######################
# df_Historic_dashboard
######################
# 'Standard amount USD' is not used in the code bellow. Only 'Currency turnover ex.VAT' is used to calculated the sales but 'Standard amount USD' to define the margin if needed later on.
df_Historic_dashboard['Order'] = df_Historic_dashboard['Order'].astype(str)
df_Historic_dashboard = df_Historic_dashboard[~df_Historic_dashboard['Order'].str.contains('NC')]
df_Historic_dashboard = df_Historic_dashboard[['Pty Indice', 'Quantity', 'Invoice date', 'Order', 'Currency turnover ex.VAT', 'Standard amount USD', 'Program', 'IDD Marge Standard', 'Product Category', 'NRE Fee', 'FAI Fee']] # 03/25
df_Historic_dashboard.rename(columns={'Quantity': 'Qty Shipped'}, inplace=True)
df_Historic_dashboard['Qty Shipped'] = df_Historic_dashboard['Qty Shipped'].astype(int)
df_Historic_dashboard['NRE Fee'] = df_Historic_dashboard['NRE Fee'].fillna(0).astype(int) # 03/25
df_Historic_dashboard['FAI Fee'] = df_Historic_dashboard['FAI Fee'].fillna(0).astype(int) # 03/25
df_Historic_dashboard.dropna(inplace=True)
df_Historic_dashboard['Invoice date'] = pd.to_datetime(df_Historic_dashboard['Invoice date'])
df_Historic_dashboard['Year'] = df_Historic_dashboard['Invoice date'].dt.year
df_Historic_dashboard['Month'] = df_Historic_dashboard['Invoice date'].dt.month
df_Historic_dashboard['Week'] = df_Historic_dashboard['Invoice date'].dt.isocalendar().week
#Rename 'Currency turnover ex.VAT' to 'Sales USD'
df_Historic_dashboard.rename(columns={'Currency turnover ex.VAT': 'Sales USD'}, inplace=True)
# 03/08
# Define the span_report_historic_dashboard place holder
span_report_historic_dashboard = "No data available after filtering"
#SAVED 03/08
'''
older_date = df_Historic_dashboard['Invoice date'].min()
recent_date = df_Historic_dashboard['Invoice date'].max()
span_report_historic_dashboard = (older_date, recent_date)
# Format the dates as short dates
older_date_str = older_date.strftime('%m/%d/%Y')
recent_date_str = recent_date.strftime('%m/%d/%Y')
# Create formatted title strings
span_report_historic_dashboard = f"{older_date_str} - {recent_date_str}"
'''
#/////////////////////////////////////////////////////////////////////////////////////////////////////////////
#################################################################################################################
# Widgets initialization and datafram update
################################################################################################################
#/////////////////////////////////////////////////////////////////////////////////////////////////////////////
#03/08 update
# Extract unique programs while ensuring only allowed ones are included
unique_programs_List = list(set(df_Priority['Program'].dropna()) & set(allowed_programs)) or ['No Program Available']
# Set default program
default_program_List = 'Phase 4-5' if 'Phase 4-5' in unique_programs_List else unique_programs_List[0]
'''
# Unique programs list from the dataframe
unique_programs_List = df_Priority['Program'].dropna().unique().tolist()
# Check if 'Phase 4-5' is in unique_programs_List, else fall back to the first item.
default_program_List = 'Phase 4-5' if 'Phase 4-5' in unique_programs_List else unique_programs_List[0]
'''
# Widget initialization
program_widget_List = pn.widgets.Select(name='Select Program', options=unique_programs_List, value=default_program_List)
###################################################################
# Update the DataFrame based on the selected program
###################################################################
# Function to update data based on the selected program
#/////////////////////////////////////////////////////////////////////////////////////////////////////////////
# New 10/01
#######################################################################################################################################################
# Create a filter on the 'Product Category' to filter-out 'Lightplate' and 'Others' from the dashboard when click on a 2 disctincts button with 2 positions: "Included (on)/Excluded (off)" Lightplate, "Included (on)/Excluded (off)" others
# Directly filter the datafram df_Priority_dashboard, df_Historic_dashboard & df_Backlog_dashboard to filter-out the necessary rows based on the buttons
#######################################################################################################################################################
# Function to filter data based on toggle buttons for 'Lightplate' and 'Others' and to modify the original dataframes
# Function to apply filters based on toggle status
#/////////////////////////////////////////////////////////////////////////////////////////////////////////////
# Data Filtering Function
def filter_dashboard(df, include_lightplate, include_others, column_name):
"""Filter the dashboard DataFrame based on the include_lightplate and include_others flags."""
if not include_lightplate:
df = df[df[column_name] != 'Lightplate']
if not include_others:
df = df[df[column_name] != 'Others']
return df
# Function to apply filters based on the toggle buttons #03/21
def apply_filters(event):
global df_Historic_dashboard, df_Priority_dashboard, df_Backlog_dashboard, df_Snapshot_KPI
# Filter each dashboard with the correct column names
filtered_Historic = filter_dashboard(
df_Historic_dashboard, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
filtered_Priority = filter_dashboard(
df_Priority_dashboard, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
filtered_Backlog = filter_dashboard(
df_Backlog_dashboard, toggle_lightplate.value, toggle_others.value, 'Product_Category'
)
# New 03/21
filtered_Snapshot = filter_dashboard(
df_Snapshot_KPI, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
# Display filtered data shapes for debugging
#print("After Filtering:")
#print(f"Historic Dashboard Shape: {filtered_Historic.shape}")
#print(f"Priority Dashboard Shape: {filtered_Priority.shape}")
#print(f"Backlog Dashboard Shape: {filtered_Backlog.shape}")
# Attach this to the toggle events
def on_lightplate_toggle(event):
update_button_styles(toggle_lightplate)
apply_filters(event) # Pass the event argument here
def on_others_toggle(event):
update_button_styles(toggle_others)
apply_filters(event) # Pass the event argument here
# Create Toggle Widgets with default button styles
toggle_lightplate = pn.widgets.Toggle(name='Include Lightplate', value=True, button_type='primary')
toggle_others = pn.widgets.Toggle(name='Include Sub-Levels & Kits', value=True, button_type='primary')
# Update the button type based on its value
def update_button_styles(toggle_widget):
"""Updates the button style to solid (active) or outline (inactive) based on the value."""
if toggle_widget.value:
toggle_widget.button_type = 'primary' # Solid fill when active
else:
toggle_widget.button_type = 'default' # Outline when inactive
# Attach the update function to the toggle widgets
toggle_lightplate.param.watch(lambda event: update_button_styles(toggle_lightplate), 'value')
toggle_others.param.watch(lambda event: update_button_styles(toggle_others), 'value')
# Initial styles application
update_button_styles(toggle_lightplate)
update_button_styles(toggle_others)
################################################################################################################
#/////////////////////////////////////////////////////////////////////////////////////////////////////////////
# Define the YoY Monthly sales as a line graph
# Define the Monthly sales since the beginning of the project as a line graph
# Define the yearly, Monthly and since the beginning of the project sales & shipement
# Define specific colors for years 2022 to 2030 - 04/09
year_color_map = {
2022: '#F4B084', # Salmon
2023: '#c4818e', # Pinkinch
2024: '#a2d5d6', # Green #Alternative #4e9376 Forest Green (earthy, stable)
2025: '#4e9376', #
2026: '#de9cdc', # Lavender Gray (soft, muted)
2027: '#88aee1', # Pastel Purple (playful, light)
2028: '#dec49f', # Pale Lilac (delicate, subtle)
2029: '#adcac9', # Dusty Teal (neutral, balanced)
2030: '#777777', # Medium Gray (solid, endpoint)
}
# Update 05/16
#---------------------------------------------------------------------------------------------------------
##################################################################
#///////////////////////////////////////////////////////////////
# Create sales and shipements Graphs YoY, MoM and total since inception
#///////////////////////////////////////////////////////////////
##################################################################
# Figure 'Year-Over-Year' monthly sales - # Update 05/16
##################################################################
def create_yoy_sales_figure(df_Historic_dashboard_filtered, selected_program, year_color_map):
# Month mapping
month_map = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'}
# Get all years and sort them
all_years = sorted(df_Historic_dashboard_filtered['Year'].unique(), reverse=False)
# Create a complete index for all Year-Month combinations
complete_index = pd.MultiIndex.from_product([all_years, range(1, 13)], names=['Year', 'Month'])
# Aggregate data and reindex to include missing months with NaN
df_YoY_sales = df_Historic_dashboard_filtered.groupby(['Year', 'Month'])['Sales USD'].sum().reindex(complete_index).reset_index()
df_YoY_sales['Month Name'] = df_YoY_sales['Month'].map(month_map)
df_YoY_sales['Sales K$'] = df_YoY_sales['Sales USD'] / 1000
# Initially set missing data to NaN
df_YoY_sales.loc[df_YoY_sales['Sales USD'].isna(), 'Sales K$'] = np.nan
# For each year, set 0 for months between two months with values, keep NaN otherwise
for year in all_years:
subset = df_YoY_sales[df_YoY_sales['Year'] == year].copy()
sales = subset['Sales K$'].values
for i in range(len(sales)):
if pd.isna(sales[i]): # If the current month is NaN
# Check if there are non-NaN values before and after
left_value = None
right_value = None
for j in range(i - 1, -1, -1): # Look left
if not pd.isna(sales[j]):
left_value = sales[j]
break
for j in range(i + 1, len(sales)): # Look right
if not pd.isna(sales[j]):
right_value = sales[j]
break
# If there are non-NaN values on both sides, set to 0
if left_value is not None and right_value is not None:
df_YoY_sales.loc[(df_YoY_sales['Year'] == year) & (df_YoY_sales['Month'] == i + 1), 'Sales K$'] = 0
# Determine the x-axis range
if selected_program == '1stTB':
# Find the last month with data
if df_YoY_sales['Sales K$'].isna().all():
last_year, last_month = all_years[-1], 12 # Default to December of the last year
else:
last_data_point = df_YoY_sales.dropna(subset=['Sales K$']).sort_values(by=['Year', 'Month']).iloc[-1]
last_year, last_month = last_data_point['Year'], last_data_point['Month']
# Define the x-axis range: start from the last month and go back 12 months
months_order = []
for i in range(12):
month_idx = (last_month - i) % 12
if month_idx == 0:
month_idx = 12 # Handle the case where modulo gives 0 (December)
months_order.insert(0, month_map[month_idx]) # Insert at the beginning to reverse the order
else:
# For other programs, use the default Jan to Dec order
months_order = list(month_map.values())
# Set the y-axis range
max_sales = df_YoY_sales['Sales K$'].max()
y_range = (0, max_sales * 1.25) if not np.isnan(max_sales) else (0, 100)
# Create the figure with the adjusted x_range
p_YoY_sales = figure(title="Year-Over-Year Monthly Sales [K$]",
x_axis_label='Month',
y_axis_label='Sales [K$]',
x_range=months_order,
y_range=y_range,
tools="pan,wheel_zoom,box_zoom,reset")
all_renderers = []
# Plot data for each year
for year in all_years:
subset = df_YoY_sales[df_YoY_sales['Year'] == year].copy()
line_color = year_color_map.get(year, 'gray')
subset['Year'] = subset['Year'].astype(str) # Ensure year is a string for hover
# Get the color for the year, default to 'gray' if not in the map
line_color = year_color_map.get(year, 'gray')
# For 1stTB program, we need to handle the circular x-axis specially
if selected_program == '1stTB':
# Split the data into two segments: Jun-Dec and Jan-May
first_half = subset[subset['Month'].between(6, 12)] # Jun-Dec
second_half = subset[subset['Month'].between(1, 5)] # Jan-May
# Plot each segment separately
if not first_half.empty:
line1 = p_YoY_sales.line(first_half['Month Name'], first_half['Sales K$'],
line_width=2, color=line_color, legend_label=str(year))
circle1 = p_YoY_sales.circle(first_half['Month Name'], first_half['Sales K$'],
size=8, color=line_color, alpha=0.8)
all_renderers.extend([line1, circle1])
if not second_half.empty:
line2 = p_YoY_sales.line(second_half['Month Name'], second_half['Sales K$'],
line_width=2, color=line_color, legend_label=str(year))
circle2 = p_YoY_sales.circle(second_half['Month Name'], second_half['Sales K$'],
size=8, color=line_color, alpha=0.8)
all_renderers.extend([line2, circle2])
else:
# For other programs, plot normally
line = p_YoY_sales.line(subset['Month Name'], subset['Sales K$'],
line_width=2, color=line_color, legend_label=str(year))
circle = p_YoY_sales.circle(subset['Month Name'], subset['Sales K$'],
size=8, color=line_color, alpha=0.8)
all_renderers.extend([line, circle])
# Add hover tool with Year, Month, and Sales
hover = HoverTool(renderers=all_renderers,
tooltips=[
('Month', '@{x}'),
('Shipments', '@y{0,0}')
],
mode='mouse')
p_YoY_sales.add_tools(hover)
# Customize the plot
p_YoY_sales.ygrid.grid_line_dash = [6, 4]
p_YoY_sales.xgrid.visible = False
p_YoY_sales.toolbar.logo = None
p_YoY_sales.legend.location = "top_left"
p_YoY_sales.legend.click_policy = "hide"
# X-axis font size
p_YoY_sales.xaxis.major_label_text_font_size = "12pt"
p_YoY_sales.xaxis.major_label_text_font_style = "bold"
# Title text font
p_YoY_sales.title.text_font_size = "14pt"
return p_YoY_sales
##################################################################
# Figure 'Year-Over-Year' monthly shipment - #Update 05/16
##################################################################
def create_yoy_shipments_figure(df_Historic_dashboard_filtered, selected_program, year_color_map):
# Month mapping
month_map = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'}
# Get all years and sort them
all_years = sorted(df_Historic_dashboard_filtered['Year'].unique(), reverse=False)
# Create a complete index for all Year-Month combinations
complete_index = pd.MultiIndex.from_product([all_years, range(1, 13)], names=['Year', 'Month'])
# Aggregate data and reindex to include missing months with NaN
df_YoY_shipments = df_Historic_dashboard_filtered.groupby(['Year', 'Month'])['Qty Shipped'].sum().reindex(complete_index).reset_index()
df_YoY_shipments['Month Name'] = df_YoY_shipments['Month'].map(month_map)
# Initially set missing data to NaN
df_YoY_shipments.loc[df_YoY_shipments['Qty Shipped'].isna(), 'Qty Shipped'] = np.nan
# For each year, set 0 for months between two months with values, keep NaN otherwise
for year in all_years:
subset = df_YoY_shipments[df_YoY_shipments['Year'] == year].copy()
shipments = subset['Qty Shipped'].values
for i in range(len(shipments)):
if pd.isna(shipments[i]): # If the current month is NaN
# Check if there are non-NaN values before and after
left_value = None
right_value = None
for j in range(i - 1, -1, -1): # Look left
if not pd.isna(shipments[j]):
left_value = shipments[j]
break
for j in range(i + 1, len(shipments)): # Look right
if not pd.isna(shipments[j]):
right_value = shipments[j]
break
# If there are non-NaN values on both sides, set to 0
if left_value is not None and right_value is not None:
df_YoY_shipments.loc[(df_YoY_shipments['Year'] == year) & (df_YoY_shipments['Month'] == i + 1), 'Qty Shipped'] = 0
# Determine the x-axis range
if selected_program == '1stTB':
# Find the last month with data
if df_YoY_shipments['Qty Shipped'].isna().all():
last_year, last_month = all_years[-1], 12 # Default to December of the last year
else:
last_data_point = df_YoY_shipments.dropna(subset=['Qty Shipped']).sort_values(by=['Year', 'Month']).iloc[-1]
last_year, last_month = last_data_point['Year'], last_data_point['Month']
# Define the x-axis range: start from the last month and go back 12 months
months_order = []
for i in range(12):
month_idx = (last_month - i) % 12
if month_idx == 0:
month_idx = 12 # Handle the case where modulo gives 0 (December)
months_order.insert(0, month_map[month_idx]) # Insert at the beginning to reverse the order
else:
# For other programs, use the default Jan to Dec order
months_order = list(month_map.values())
# Set the y-axis range
max_shipments = df_YoY_shipments['Qty Shipped'].max()
y_range = (0, max_shipments * 1.25) if not np.isnan(max_shipments) else (0, 100)
# Create the figure with the adjusted x_range
p_YoY_shipments = figure(title="Year-Over-Year Monthly Shipments [quantity shipped]",
x_axis_label='Month',
y_axis_label='Shipments',
x_range=months_order,
y_range=y_range,
tools="pan,wheel_zoom,box_zoom,reset")
all_renderers = []
# Plot data for each year
for year in all_years:
subset = df_YoY_shipments[df_YoY_shipments['Year'] == year].copy()
subset['Year'] = subset['Year'].astype(str) # Ensure year is a string for hover
# Get the color for the year, default to 'gray' if not in the map
line_color = year_color_map.get(year, 'gray')
# For 1stTB program, split the data into two segments to avoid connecting May to June
if selected_program == '1stTB':
# Split the data into two segments: Jun-Dec and Jan-May
first_half = subset[subset['Month'].between(6, 12)] # Jun-Dec
second_half = subset[subset['Month'].between(1, 5)] # Jan-May
# Plot each segment separately
if not first_half.empty:
line1 = p_YoY_shipments.line(first_half['Month Name'], first_half['Qty Shipped'],
line_width=2, color=line_color, legend_label=str(year))
circle1 = p_YoY_shipments.circle(first_half['Month Name'], first_half['Qty Shipped'],
size=8, color=line_color, alpha=0.8)
all_renderers.extend([line1, circle1])
if not second_half.empty:
line2 = p_YoY_shipments.line(second_half['Month Name'], second_half['Qty Shipped'],
line_width=2, color=line_color)
circle2 = p_YoY_shipments.circle(second_half['Month Name'], second_half['Qty Shipped'],
size=8, color=line_color, alpha=0.8)
all_renderers.extend([line2, circle2])
else:
# For other programs, plot normally
line = p_YoY_shipments.line(subset['Month Name'], subset['Qty Shipped'],
line_width=2, color=line_color, legend_label=str(year))
circle = p_YoY_shipments.circle(subset['Month Name'], subset['Qty Shipped'],
size=8, color=line_color, alpha=0.8)
all_renderers.extend([line, circle])
# Add hover tool with Year, Month, and Shipments
hover = HoverTool(renderers=all_renderers,
tooltips=[
('Month', '@{x}'),
('Shipments', '@y{0,0}')
],
mode='mouse')
p_YoY_shipments.add_tools(hover)
# Customize the plot
p_YoY_shipments.ygrid.grid_line_dash = [6, 4]
p_YoY_shipments.xgrid.visible = False
p_YoY_shipments.toolbar.logo = None
p_YoY_shipments.legend.location = "top_left"
p_YoY_shipments.legend.click_policy = "hide"
# X-axis font size
p_YoY_shipments.xaxis.major_label_text_font_size = "12pt"
p_YoY_shipments.xaxis.major_label_text_font_style = "bold"
# Title text font
p_YoY_shipments.title.text_font_size = "14pt"
return p_YoY_shipments
#---------------------------------------------------------------------------------------------------------
##################################################################
#///////////////////////////////////////////////////////////////
# Create costumers Graph
#///////////////////////////////////////////////////////////////
##################################################################
#######################################################################################
# Define the total shipement and total sales per costumers
#######################################################################################
# Card 'Costumers'
#################################
#////////////////////////////////
################################
# Integrate logo to the graph
###############################
#////////////////////////////////
# Created 09/06
def image_to_base64(image_path):
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def scale_image(image_base64, max_width, max_height):
# Decode base64 image
image_data = base64.b64decode(image_base64)
image = Image.open(io.BytesIO(image_data))
# Calculate new dimensions maintaining aspect ratio
width, height = image.size
scaling_factor = min(max_width / width, max_height / height)
new_width = int(width * scaling_factor)
new_height = int(height * scaling_factor)
# Resize image with LANCZOS resampling
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Encode image back to base64
buffered = io.BytesIO()
image.save(buffered, format="PNG")
new_image_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
return f'data:image/png;base64,{new_image_base64}'
def create_logo_mapping(image_directory, max_width=30, max_height=30):
logo_mapping = {}
for image_name in os.listdir(image_directory):
if image_name.endswith(('png', 'jpg', 'jpeg')):
customer_name = os.path.splitext(image_name)[0]
image_path = os.path.join(image_directory, image_name)
base64_image = image_to_base64(image_path)
scaled_image = scale_image(base64_image, max_width, max_height)
logo_mapping[customer_name] = scaled_image
return logo_mapping
####################################
# Adjust the path to your directory
######################################
current_folder = os.getcwd() # Get the current working directory
image_directory = os.path.join(current_folder, 'Compressed_Images')
# Print all files in the directory to ensure the path is correct
#print(os.listdir(image_directory))
# Create logo mapping
logo_mapping = create_logo_mapping(image_directory)
#define the default logo offset on top of the bars
#default_logo_offset = 50
# Add this after creating logo_mapping to inspect
#print(logo_mapping)
######################################
#--------------------------------------------------------------------------------------------------------------------------------------------------------
# 04/07
#--------------------------------------------------------------------------------------------------------------------------------------------------------
# Update the span of the graph.
# Need to use df_Historic_dashboard_filtered to get to the number of unit shipped in a given span and df df_Historic_dashboard_filtered to get to the customer related to a given Pty Indice
# Use a limited span for selected_program == '1stTB'
#04/07
def create_customers_figure(df_priority_dashboard_filtered, logo_mapping, df_Historic_dashboard_filtered, selected_program):
"""
Creates customer shipment and sales figures using:
- Customer info from priority dashboard (df_priority_dashboard_filtered)
- Shipment data from historic dashboard (df_Historic_dashboard_filtered)
- Logo mappings (logo_mapping)
Returns:
- p_shipment: Figure showing shipment quantities by customer
- p_sales: Figure showing sales amounts by customer
"""
# 1. Get customer mapping from priority dashboard
customer_mapping = (
df_priority_dashboard_filtered[['Pty Indice', 'End Costumer']].drop_duplicates()
.rename(columns={'End Costumer': 'End_Customer'})
)
# 2. Aggregate historical shipment data by Pty Indice
historic_shipments = (
df_Historic_dashboard_filtered.groupby('Pty Indice')
.agg({'Qty Shipped': 'sum', 'Sales USD': 'sum'})
.reset_index()
)
# 3. Merge to attach customer names to shipment data
df_customer_summary = pd.merge(
historic_shipments, customer_mapping, on='Pty Indice', how='inner'
)
# 4. Group by customer (summing quantities and sales)
df_customer_summary = df_customer_summary.groupby('End_Customer').agg({
'Qty Shipped': 'sum', 'Sales USD': 'sum'
}).reset_index()
# 5. Prepare plot data (convert sales to K$)
df_customer_summary['Sales_USD'] = df_customer_summary['Sales USD'] / 1000
df_customer_summary['Logo'] = df_customer_summary['End_Customer'].map(logo_mapping)
# Calculate max values for plot scaling
max_qty_shipped = df_customer_summary['Qty Shipped'].max()
max_sales_usd = df_customer_summary['Sales_USD'].max()
# Calculate logo offsets
logo_offset_shipment = max(max_qty_shipped * 0.1, 20)
logo_offset_sale = max(max_sales_usd * 0.1, 10)
df_customer_summary['Logo_Offset_Shipments'] = df_customer_summary['Qty Shipped'] + logo_offset_shipment
df_customer_summary['Logo_Offset_Sales'] = df_customer_summary['Sales_USD'] + logo_offset_sale
# Create color palette
customers = df_customer_summary['End_Customer'].unique()
num_customers = len(customers)
base_palette = Category20[max(3, min(20, num_customers))]
pastel_colors = []
for color in base_palette[:num_customers]:
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
r = int(r + (255 - r) * 0.6)
g = int(g + (255 - g) * 0.6)
b = int(b + (255 - b) * 0.6)
pastel_colors.append(f'#{r:02x}{g:02x}{b:02x}')
customer_color_map = dict(zip(customers, pastel_colors))
df_customer_summary['Color'] = df_customer_summary['End_Customer'].map(customer_color_map)
# Create data source for plots
source = ColumnDataSource(df_customer_summary)
# Convert 'Invoice date' to datetime
df_Historic_dashboard_filtered['Invoice date'] = pd.to_datetime(df_Historic_dashboard_filtered['Invoice date'], format='%m/%d/%Y')
# Extract the earliest and latest months
earliest_month = df_Historic_dashboard_filtered['Invoice date'].min().strftime('%b %Y')
latest_month = df_Historic_dashboard_filtered['Invoice date'].max().strftime('%b %Y')
#print(f'Earliest Month: {earliest_month}, Latest Month: {latest_month}')
# Create shipment plot with a plain text title
p_shipment = figure(
x_range=df_customer_summary['End_Customer'].tolist(),
title=f"Total Shipment per Customer, Program: {selected_program} - [{earliest_month} to {latest_month}]",
x_axis_label='Customer',
y_axis_label='Shipments',
tools="pan,wheel_zoom,save,reset",
y_range=(0, max_qty_shipped * 1.5),
height=400,
width=800,
min_height=400, # Ensure the plot doesn't shrink below 400px in height
output_backend='svg'
)
p_shipment.vbar(
x='End_Customer',
top='Qty Shipped',
width=0.4,
source=source,
fill_color='Color',
line_color='Color',
alpha=0.6
)
p_shipment.add_tools(HoverTool(
tooltips=[('Customer', '@End_Customer'), ('Qty Shipped', '@{Qty Shipped}{0,0}')]
))
p_shipment.add_glyph(
source,
ImageURL(
url='Logo',
x='End_Customer',
y='Logo_Offset_Shipments',
anchor="center",
)
)
# Create sales plot with a plain text title
p_sales = figure(
x_range=df_customer_summary['End_Customer'].tolist(),
title=f"Total Sales per Customer, Program: {selected_program} - [{earliest_month} to {latest_month}]",
x_axis_label='Customer',
y_axis_label='Sales [K$]',
tools="pan,wheel_zoom,save,reset",
y_range=(0, max_sales_usd * 1.5),
height=400,
width=800,
min_height=400, # Ensure the plot doesn't shrink below 400px in height
output_backend='svg'
)
p_sales.vbar(
x='End_Customer',
top='Sales_USD',
width=0.4,
source=source,
fill_color='Color',
line_color='Color',
alpha=0.6
)
p_sales.add_tools(HoverTool(
tooltips=[('Customer', '@End_Customer'), ('Sales', '@Sales_USD{($0,0.0)}K')]
))
p_sales.add_glyph(
source,
ImageURL(
url='Logo',
x='End_Customer',
y='Logo_Offset_Sales',
anchor="center",
)
)
# Common formatting for both plots
for plot in [p_shipment, p_sales]:
#plot.title.text_color = "#000000"
plot.title.text_font_size = "14pt"
plot.xaxis.major_label_text_font_size = '12pt'
plot.xaxis.major_label_text_font_style = "bold"
plot.xaxis.major_label_orientation = 1.2
plot.ygrid.grid_line_dash = [6, 4]
plot.xgrid.visible = False
plot.toolbar.logo = None
p_shipment.yaxis.formatter = NumeralTickFormatter(format="0,0")
p_sales.yaxis.formatter = CustomJSTickFormatter(code="return '$' + (tick).toFixed(0) + 'k';")
return p_shipment, p_sales
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------------------------------------
# 03/04 - Create new datafram for RMA (NC) KPI
#-----------------------------------------------------------------------------------------------------------
def load_and_preprocess_nc_data(input_file_formatted):
"""
Load and preprocess the RMA (NC) data from the Excel file.
Args:
input_file_formatted (str): Path to the formatted Excel file.
Returns:
pd.DataFrame: Preprocessed DataFrame for RMA (NC) data.
"""
try:
# Load data
df_Historic_NC = pd.read_excel(input_file_formatted, sheet_name='CM-Historic', index_col=False)
# Preprocess df_Historic_NC to combine "Phase 4" and "Phase 5" into "Phase 4-5"
if 'Program' in df_Historic_NC.columns and 'Pty Indice' in df_Historic_NC.columns:
mask = (
df_Historic_NC['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Historic_NC['Pty Indice'].str.contains('Phase5', na=False)
)
df_Historic_NC.loc[mask, 'Program'] = 'Phase 4-5'
# Filter rows where 'Order' contains 'NC'
df_Historic_NC = df_Historic_NC[df_Historic_NC['Order'].str.contains('NC', na=False)]
# Ensure 'Quantity' is numeric and filter out rows where 'Quantity' <= 0
df_Historic_NC['Quantity'] = pd.to_numeric(df_Historic_NC['Quantity'], errors='coerce')
df_Historic_NC = df_Historic_NC[df_Historic_NC['Quantity'] > 0]
# Ensure 'Invoice date' is a datetime column
df_Historic_NC['Invoice date'] = pd.to_datetime(df_Historic_NC['Invoice date'], errors='coerce')
# Extract 'Year' and 'Month' from the 'Invoice date' column
df_Historic_NC['Year'] = df_Historic_NC['Invoice date'].dt.year
df_Historic_NC['Month'] = df_Historic_NC['Invoice date'].dt.month
return df_Historic_NC
except Exception as e:
print(f"Error loading or preprocessing RMA (NC) data: {e}")
return pd.DataFrame() # Return an empty DataFrame in case of error
# Load and preprocess the RMA (NC) data
df_Historic_NC = load_and_preprocess_nc_data(input_file_formatted)
#-----------------------------------------------------------------------------------------------------------
# 03/06 - Create bar graph with quantity of NC received Monthly
#-----------------------------------------------------------------------------------------------------------
def create_card_graph_RMA(df_Historic_NC_filtered, year_color_map):
"""
Create a card displaying the monthly RMA graph.
Args:
df_Historic_NC_filtered (pd.DataFrame): Filtered RMA (NC) data.
year_color_map (dict): Mapping of years to colors.
Returns:
figure: Bokeh figure object for the RMA graph.
"""
try:
# Aggregate the data by Year and Month
df_NC_monthly = df_Historic_NC_filtered.groupby(['Year', 'Month']).agg({'Quantity': 'sum'}).reset_index()
# Convert month number to month name and format as "Month Year"
df_NC_monthly['Year-Month'] = df_NC_monthly.apply(
lambda row: f"{calendar.month_abbr[row['Month']]} {row['Year']}", axis=1
)
# Get unique years that have data
available_years = df_NC_monthly['Year'].unique()
# Create the figure
p_NC_monthly = figure(
x_range=df_NC_monthly['Year-Month'].unique().tolist(), # X-axis: Year-Month
title="Monthly Quantity of RMA Received",
x_axis_label='Month',
y_axis_label='Quantity of RMA',
tools="pan,wheel_zoom,box_zoom,reset,save",
width=1200, # 03/25
height=650, #03/25
sizing_mode='fixed', # Explicitly fixed size
)
# Add bars and legend only for available years
for year in available_years:
if year in year_color_map: # Ensure the year has a defined color
df_year = df_NC_monthly[df_NC_monthly['Year'] == year]
source = ColumnDataSource(df_year)
p_NC_monthly.vbar(
x='Year-Month',
top='Quantity',
width=0.9,
source=source,
fill_color=year_color_map[year],
line_color=year_color_map[year],
legend_label=str(year) # Legend entry only for existing years
)
# Add hover tool
hover = HoverTool()
hover.tooltips = [
("Year-Month", "@{Year-Month}"),
("Quantity", "@Quantity")
]
p_NC_monthly.toolbar.logo = None
p_NC_monthly.add_tools(hover)
# Customize the legend
p_NC_monthly.legend.title = "Year"
p_NC_monthly.legend.label_text_font_size = "10pt"
p_NC_monthly.legend.location = "top_left" # Legend in top left
p_NC_monthly.legend.background_fill_alpha = 0.3 # Semi-transparent background
# Customize the plot
p_NC_monthly.xaxis.major_label_orientation = 1.2 # Rotate x-axis labels
p_NC_monthly.xgrid.visible = False # Hide x-axis grid lines
p_NC_monthly.ygrid.grid_line_dash = [6, 4] # Customize y-axis grid lines
# X-axis font size 04/02
p_NC_monthly.xaxis.major_label_text_font_size = "12pt"
p_NC_monthly.xaxis.major_label_text_font_style = "bold"
# Title text font
p_NC_monthly.title.text_font_size = "14pt"
return p_NC_monthly
except Exception as e:
print(f"Error creating monthly RMA graph: {e}")
return None
#Update 03/17
def create_yearly_rma_card(df_Historic_NC_filtered, year_color_map, yearly_sales_shipment_data):
"""
Create a card displaying the total NC items received each year in the format "Year X: Y RMA received, Z% of total units shipped".
Args:
df_Historic_NC_filtered (pd.DataFrame): Filtered RMA (NC) data.
year_color_map (dict): Mapping of years to colors.
yearly_sales_shipment_data (dict): Data containing total units shipped per year.
Returns:
pn.pane.HTML: Panel card displaying yearly RMA metrics.
"""
try:
# Aggregate RMA (NC) data by year
df_NC_yearly = df_Historic_NC_filtered.groupby('Year').agg({'Quantity': 'sum'}).reset_index()
# Generate content for the card
content = """
<div style="font-size: 20px; font-family: Arial, sans-serif; padding: 10px; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;">
<h3 style="margin: 0; color: teal;">Yearly RMA</h3>
"""
for _, row in df_NC_yearly.iterrows():
year = row['Year']
quantity = row['Quantity']
total_shipped = yearly_sales_shipment_data.get(year, {}).get('total_shipped', 0)
percentage = (quantity / total_shipped * 100) if total_shipped > 0 else 0
year_color = year_color_map.get(year, 'gray') # Default to gray if year not in map
content += f"""
<p style="margin: 10px 0; font-size: 16px;">
<span style="color: {year_color}; font-weight: bold;">Year {year}:</span>
<strong>{quantity:,.0f}</strong> RMA received, <strong>{percentage:,.1f}%</strong> of total units shipped
</p>
"""
# Close the HTML content
content += "</div>"
# Create a Panel card
yearly_rma_card = pn.pane.HTML(content, sizing_mode='stretch_width')
return yearly_rma_card
except Exception as e:
print(f"Error creating yearly RMA card: {e}")
return pn.pane.HTML("<div>Error loading yearly RMA data.</div>", sizing_mode='stretch_width')
#--------------------------------------------------------------------------
# 03/19 - Create a new card 'Yearly KPI - Backlog' based on df_Backlog_KPI
#--------------------------------------------------------------------------
df_Backlog_KPI = df_Backlog.copy() # Use .copy() to create a copy of the dataframe
# Rename 'Product_Category' to 'Product Category'
df_Backlog_KPI = df_Backlog_KPI.rename(columns={'Product_Category': 'Product Category'})
# Filter out rows where 'Order' contains 'NC'
df_Backlog_KPI = df_Backlog_KPI[~df_Backlog_KPI['Order'].str.contains('NC', na=False)]
# Filter out rows where 'Order' starts with 'E', this is for prototyping
df_Backlog_KPI = df_Backlog_KPI[~df_Backlog_KPI['Order'].str.startswith('E', na=False)]
# Create column 'Year' in df_Backlog_KPI based on ['Due Date']
# Ensure 'Due Date' is in datetime format
df_Backlog_KPI['Due Date'] = pd.to_datetime(df_Backlog_KPI['Due Date'])
# Extract the year from 'Due Date' and create a new 'Year' column
df_Backlog_KPI['Year'] = df_Backlog_KPI['Due Date'].dt.year
#display(df_Backlog_KPI.head()) # Check the first few rows
#print(df_Backlog_KPI.columns) # Verify the columns
#--------------------------------------------------------------------------
# Step 1: Define the function to calculate backlog metrics
def calculate_backlog_metrics(df_Backlog):
# Ensure the required columns exist
required_columns = ['Year', 'Backlog Qty', 'Currency net amount']
for col in required_columns:
if col not in df_Backlog.columns:
raise KeyError(f"The column '{col}' is missing in the backlog dataframe.")
# Group by year and calculate total backlog units and sales
backlog_metrics = df_Backlog.groupby('Year').agg({
'Backlog Qty': 'sum', # Total backlog units
'Currency net amount': 'sum' # Total backlog sales
}).reset_index()
# Convert the metrics to a dictionary for easy access
backlog_metrics_dict = backlog_metrics.set_index('Year').to_dict(orient='index')
return backlog_metrics_dict
# Step 2: Create the HTML content for the card
def create_backlog_card(df_Backlog, year_color_map, file_date):
try:
Report_date = file_date
# Calculate backlog metrics
backlog_metrics = calculate_backlog_metrics(df_Backlog)
# Ensure file_date is a date object
Report_date = pd.to_datetime(Report_date).date()
# Calculate past due backlog
past_due_backlog = df_Backlog[df_Backlog['Due Date'].dt.date < Report_date]
past_due_units = past_due_backlog['Backlog Qty'].sum()
past_due_sales = past_due_backlog['Currency net amount'].sum()
except KeyError as e:
# Handle missing columns
return pn.pane.HTML(f"<div style='color: red;'>Error: {str(e)}</div>")
# Create HTML content for the card (without the title)
html_content = f"""
<div style="font-size: 20px; font-family: Arial, sans-serif; padding: 10px; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;">
<p style="margin: 10px 0; font-size: 16px;">
<span style="color: black; font-weight: bold;">Past Due Backlog</span> [{file_date}]:
<strong>{past_due_units:,.0f}</strong> units, representing <strong>${past_due_sales / 1000:,.1f}K</strong> in sales
</p>
<hr style="border: none; height: 0.5px; background-color: #ddd; margin: 10px 0;"> <!-- Thinner gray divider line -->
"""
# Add metrics for each year with color coding
for year, metrics in backlog_metrics.items():
if metrics['Backlog Qty'] == 0 and metrics['Currency net amount'] == 0:
continue # Skip years with zero backlog
# Get the color for the year from the year_color_map
year_color = year_color_map.get(year, 'black') # Default to black if year not in map
html_content += f"""
<p style="margin: 10px 0; font-size: 16px;">
<span style="color: {year_color}; font-weight: bold;">Year {year}:</span>
<strong>{metrics['Backlog Qty']:,.0f}</strong> units in backlog, representing <strong>${metrics['Currency net amount'] / 1000:,.1f}K</strong> in sales
</p>
"""
# Close the HTML content
html_content += "</div>"
# Create a layout with the HTML content
backlog_card_layout = pn.Column(
pn.pane.HTML(html_content, sizing_mode='stretch_width'),
sizing_mode='stretch_width'
)
return backlog_card_layout
#--------------------------------------------------------------------------
#-----------------------------------------------------------------------------------------------------------
# 03/31WIP - Create New Graph based on Sciforma Report
#-----------------------------------------------------------------------------------------------------------
# Load df_AccessDB
df_AccessDB = pd.read_excel(input_file_formatted, sheet_name='AccessReport', index_col=False)
# Combine phase 4 and 5 to Phase 4-5
if 'Program' in df_AccessDB.columns and 'Pty Indice' in df_AccessDB.columns:
mask = (
df_AccessDB['Program'].isin(['Phase 4', 'Phase 5']) &
~df_AccessDB['Pty Indice'].str.contains('Phase5', na=False)
)
df_AccessDB.loc[mask, 'Program'] = 'Phase 4-5'
date_columns = ['Date to ENG', 'Date completed', 'Date Order']
for col in date_columns:
df_AccessDB[col] = pd.to_datetime(df_AccessDB[col], errors='coerce')
def calculate_working_days(start_date, end_date):
if pd.isna(start_date) or pd.isna(end_date):
return np.nan
# If inputs are strings, convert to datetime first
if isinstance(start_date, str):
start_date = pd.to_datetime(start_date, errors='coerce')
if isinstance(end_date, str):
end_date = pd.to_datetime(end_date, errors='coerce')
# If conversion failed, return NaN
if pd.isna(start_date) or pd.isna(end_date):
return np.nan
return np.busday_count(start_date.date(), end_date.date())
# Apply the function to calculate completion time and order to ENG time
df_AccessDB['Completion Time'] = df_AccessDB.apply(
lambda row: calculate_working_days(row['Date to ENG'], row['Date completed']),
axis=1
)
df_AccessDB['Order to ENG Time'] = df_AccessDB.apply(
lambda row: calculate_working_days(row['Date Order'], row['Date to ENG']),
axis=1
)
# Filter the rows where we have valid data (no missing 'Date completed' or 'Date to ENG')
valid_rows = df_AccessDB.dropna(subset=['Completion Time'])
# Create the 'Month' column from 'Date completed'
df_AccessDB['Month_completed'] = df_AccessDB['Date completed'].dt.strftime('%b-%Y')
# Data preparation
#print('df_AccessDB:')
#display(df_AccessDB)
#-----------------------------------------------------------------------------------------------------------
# Load SciformaReport tab to get the NRE data
df_SciformaReport = pd.read_excel(input_file_formatted, sheet_name='SciformaSummary', index_col=False)
#-----------------------------------------------------------------------------------------------------------
# Create a HTMl text bellow the graph create_monthly_nre_fees_graph with:
# Average NRE fees charged monthly:
# Average NRE spent monthly:
# Average ECO released monthly:
# Quantity ECO released on the period: based on 'Change#' from df_AccessDB for a given program (column 'Program'):
# divider gray light
# Theoritical fees to be charged ($5K per ECO): quantity ECO released * $5K
# Fees charged vs. theoritical: {percentage}
# 04/14 - Dashed Line Inconsistency
def create_monthly_nre_fees_graph(df_Historic_dashboard_filtered, df_SciformaReport_filtered, df_AccessDB_filtered, year_color_map, selected_program):
"""
Creates a bar chart showing monthly NRE fees and NRE spending (both in $K) for the selected program,
with horizontal dashed purple bars for average NRE Fees and NRE Spending, and embeds HTML text with metrics below the graph.
Args:
df_Historic_dashboard_filtered: Filtered historical data DataFrame
df_SciformaReport_filtered: DataFrame containing program hours per month
df_AccessDB: DataFrame containing ECO data
year_color_map: Dictionary mapping years to colors
selected_program: Program name (e.g., "1stTB")
Returns:
p: Bokeh layout object (column) containing the figure and HTML text Div
"""
# --- Data Preparation for NRE Fees (from df_Historic_dashboard_filtered) ---
df = df_Historic_dashboard_filtered.copy()
df['YearMonth'] = df['Invoice date'].dt.to_period('M')
# Group by YearMonth and calculate NRE fees
monthly_data = df.groupby('YearMonth').agg({
'NRE Fee': 'sum'
}).reset_index()
# Convert to $K for display
monthly_data['NRE_Fees_K'] = monthly_data['NRE Fee'] / 1000 # Convert to $K
monthly_data['NRE_Display'] = monthly_data['NRE_Fees_K'] # Already in $K
# Get the date span from df_SciformaReport_filtered
month_columns = [col for col in df_SciformaReport_filtered.columns if col.startswith('202')]
earliest_month = pd.to_datetime(month_columns[0], format='%Y-%m').strftime('%b %Y')
latest_month = pd.to_datetime(month_columns[-1], format='%Y-%m').strftime('%b %Y')
# Create a full range of months
start_date = monthly_data['YearMonth'].min()
end_date = monthly_data['YearMonth'].max()
if pd.isna(start_date) or pd.isna(end_date):
start_date = pd.Period(month_columns[0], freq='M')
end_date = pd.Period(month_columns[-1], freq='M')
all_months = pd.period_range(start=start_date, end=end_date, freq='M')
all_months_df = pd.DataFrame({'YearMonth': all_months})
# Merge with all months to ensure no months are missing
monthly_data = all_months_df.merge(monthly_data, on='YearMonth', how='left').fillna({'NRE_Fees_K': 0, 'NRE_Display': 0})
# Create display columns
monthly_data['Date'] = monthly_data['YearMonth'].dt.to_timestamp()
monthly_data['Month_Year'] = monthly_data['Date'].dt.strftime('%b %Y')
monthly_data['Year'] = monthly_data['Date'].dt.year
monthly_data['Color'] = monthly_data['Year'].map(year_color_map).fillna('gray')
# --- Data Preparation for NRE Spending (from df_SciformaReport_filtered) ---
df_SciformaReport_filtered = df_SciformaReport_filtered.copy()
# Melt the DataFrame to get months as rows
spending_data = df_SciformaReport_filtered.melt(id_vars=['Program'], value_vars=month_columns,
var_name='Month', value_name='Hours')
# Calculate NRE Spending as $132 * Hours and convert to $K
spending_data['NRE_Spending'] = spending_data['Hours'] * 132 # Full dollars
spending_data['NRE_Spending_K'] = spending_data['NRE_Spending'] / 1000 # Convert to $K
# Convert Month to datetime and format for consistency
spending_data['Date'] = pd.to_datetime(spending_data['Month'], format='%Y-%m')
spending_data['Month_Year'] = spending_data['Date'].dt.strftime('%b %Y')
spending_data['Year'] = spending_data['Date'].dt.year
spending_data['Color'] = spending_data['Year'].map(year_color_map).fillna('gray')
# Create a full range of months for spending data
spending_data['YearMonth'] = spending_data['Date'].dt.to_period('M')
spending_data = all_months_df.merge(spending_data, on='YearMonth', how='left').fillna({'NRE_Spending_K': 0, 'Hours': 0})
spending_data['Date'] = spending_data['YearMonth'].dt.to_timestamp()
spending_data['Month_Year'] = spending_data['Date'].dt.strftime('%b %Y')
spending_data['Year'] = spending_data['Date'].dt.year
spending_data['Color'] = spending_data['Year'].map(year_color_map).fillna('gray')
# Merge the two datasets on Month_Year to ensure alignment
combined_data = monthly_data.merge(spending_data[['Month_Year', 'NRE_Spending_K', 'Hours']],
on='Month_Year', how='left')
combined_data['NRE_Spending_K'] = combined_data['NRE_Spending_K'].fillna(0)
combined_data['Hours'] = combined_data['Hours'].fillna(0)
# Filter out months where both NRE Fees and NRE Spending are 0
combined_data = combined_data[(combined_data['NRE_Fees_K'] > 0) | (combined_data['NRE_Spending_K'] > 0)]
# If no data remains after filtering, handle the edge case
if combined_data.empty:
raise ValueError("No data to display after filtering. Both NRE Fees and NRE Spending are 0 for all months.")
# Update the earliest and latest months based on the filtered data
earliest_month = combined_data['Date'].min().strftime('%b %Y')
latest_month = combined_data['Date'].max().strftime('%b %Y')
# --- Calculate Averages for Plotting ---
avg_nre_fees = combined_data['NRE_Fees_K'].mean()
avg_nre_spending = combined_data['NRE_Spending_K'].mean()
# --- Create Bokeh Figure ---
p = figure(
x_range=combined_data['Month_Year'],
title=f"Monthly NRE Fees vs. NRE Spending (@$132/h), Program: {selected_program} - [{earliest_month} to {latest_month}]",
width=1200,
height=650,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='right',
x_axis_label='Month',
y_axis_label='Amount ($K)',
y_range=(0, max(combined_data['NRE_Fees_K'].max(), combined_data['NRE_Spending_K'].max(), avg_nre_fees, avg_nre_spending) * 1.3),
sizing_mode='fixed',
output_backend='svg'
)
# X-axis font size
p.xaxis.major_label_text_font_size = "12pt"
p.xaxis.major_label_text_font_style = "bold"
# Title text font
p.title.text_font_size = "14pt"
# Customize grid
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.xgrid.visible = False
# Create ColumnDataSource
source = ColumnDataSource(combined_data)
# Create legend items and renderers
nre_legend_items = []
spending_legend_items = []
renderers = []
# Visualization: NRE Fees bars by year with hatch pattern
for year in sorted(combined_data['Year'].unique()):
year_data = combined_data[combined_data['Year'] == year]
year_source = ColumnDataSource(year_data)
color = year_color_map.get(year, 'gray')
# NRE Fees bars with hatch pattern
nre_bar = p.vbar(
x='Month_Year',
width=0.4,
bottom=0,
top='NRE_Fees_K',
source=year_source,
color=color,
alpha=0.3,
hatch_pattern="/",
hatch_color=color,
hatch_alpha=0.7,
hatch_weight=0.8,
name=f"nre_{year}"
)
nre_legend_items.append((f"NRE Fees {year}", [nre_bar]))
renderers.append(nre_bar)
# NRE Spending bars (solid fill)
spending_bar = p.vbar(
x='Month_Year',
width=0.4,
bottom=0,
top='NRE_Spending_K',
source=year_source,
color=color,
alpha=0.8,
name=f"spending_{year}"
)
spending_legend_items.append((f"NRE Spending {year}", [spending_bar]))
renderers.append(spending_bar)
# Adjust bar positions to avoid overlap
for r in renderers:
if "nre_" in r.name:
r.glyph.x = dodge('Month_Year', -0.2, range=p.x_range) # Shift NRE Fees bars left
elif "spending_" in r.name:
r.glyph.x = dodge('Month_Year', 0.2, range=p.x_range) # Shift NRE Spending bars right
# Add horizontal dashed purple bars for averages using Span
avg_nre_fees_span = Span(
location=avg_nre_fees,
dimension='width',
line_color='purple',
line_dash=[6, 4], # Consistent dash pattern
line_alpha=0.3,
line_width=2,
name='avg_nre_fees'
)
p.add_layout(avg_nre_fees_span)
avg_nre_spending_span = Span(
location=avg_nre_spending,
dimension='width',
line_color='purple',
line_dash=[6, 4], # Consistent dash pattern
line_alpha=0.8,
line_width=2,
name='avg_nre_spending'
)
p.add_layout(avg_nre_spending_span)
# Add a transparent line for hover consistency
hover_line = p.line(
x='Month_Year',
y='NRE_Fees_K',
source=source,
line_alpha=0 # Fully transparent
)
# Attach hover tool
hover = HoverTool(
tooltips=[
("Period", "@Month_Year"),
("NRE Fees", "@NRE_Display{$0,0.0}K"),
("NRE Spending", "@NRE_Spending_K{$0,0.0}K"),
("Hours", "@Hours{0.0}")
],
mode='vline',
renderers=[hover_line]
)
p.add_tools(hover)
# Remove Bokeh logo
p.toolbar.logo = None
# Create legends
nre_legend = Legend(
items=nre_legend_items,
location=(50, 420),
title="NRE Fees ($)",
title_text_font_size="12pt",
label_text_font_size="10pt",
glyph_height=20,
glyph_width=20,
spacing=5,
background_fill_alpha=0.7
)
spending_legend = Legend(
items=spending_legend_items,
location=(200, 420),
title="NRE Spending ($) - @$132/h",
title_text_font_size="12pt",
label_text_font_size="10pt",
glyph_height=20,
glyph_width=20,
spacing=5,
background_fill_alpha=0.7
)
# Dedicated legend for averages
avg_data_for_legend = pd.DataFrame({
'x': [combined_data['Month_Year'].iloc[0], combined_data['Month_Year'].iloc[-1]],
'y_fees': [0, 0], # y-values don’t matter since not rendered
'y_spending': [0, 0]
})
avg_legend_source = ColumnDataSource(avg_data_for_legend)
# Proxy lines for the legend (not rendered on the plot)
avg_nre_fees_line = p.line(
x='x',
y='y_fees',
source=avg_legend_source,
line_color='purple',
line_dash=[6, 4], # Match the dash pattern
line_alpha=0.3, # Match the alpha
line_width=2,
name='avg_nre_fees_legend',
visible=True # Only for legend, not rendered on plot
)
avg_nre_spending_line = p.line(
x='x',
y='y_spending',
source=avg_legend_source,
line_color='purple',
line_dash=[6, 4], # Match the dash pattern
line_alpha=0.8, # Match the alpha
line_width=2,
name='avg_nre_spending_legend',
visible=True # Only for legend, not rendered on plot
)
avg_legend_items = [
(f"Average NRE Fees: ${avg_nre_fees:,.1f}K", [avg_nre_fees_line]),
(f"Average NRE Spending: ${avg_nre_spending:,.1f}K", [avg_nre_spending_line])
]
avg_legend = Legend(
items=avg_legend_items,
location=(500, 420),
title="Averages fees on the period ($)",
title_text_font_size="12pt",
label_text_font_size="10pt",
glyph_height=20,
glyph_width=20,
spacing=5,
background_fill_alpha=0.7
)
p.add_layout(nre_legend)
p.add_layout(spending_legend)
p.add_layout(avg_legend)
# Final styling
p.xaxis.major_label_orientation = 1.2
p.yaxis.formatter = NumeralTickFormatter(format="$0a")
p.legend.click_policy = "hide"
# --- Calculate Metrics for HTML Text ---
# Average NRE Fees Charged Monthly ($K)
total_nre_fees = df_Historic_dashboard_filtered['NRE Fee'].sum() / 1000 # Convert to $K
num_months = len(combined_data['YearMonth'].unique())
avg_nre_fees_monthly = total_nre_fees / num_months if num_months > 0 else 0
# Average NRE Spent Monthly ($K)
total_nre_spending = spending_data['NRE_Spending_K'].sum()
avg_nre_spending_monthly = total_nre_spending / num_months if num_months > 0 else 0
# Initialize ECO-related metrics
avg_eco_monthly = 0
total_eco = 0
theoretical_fees = 0
fees_vs_theoretical = 0
# Process ECO data only if df_AccessDB_filtered is valid
if not df_AccessDB_filtered.empty and 'Program' in df_AccessDB_filtered.columns and 'Date completed' in df_AccessDB_filtered.columns:
try:
eco_data = df_AccessDB_filtered[df_AccessDB_filtered['Program'] == selected_program].copy()
if not eco_data.empty:
eco_data['YearMonth'] = pd.to_datetime(eco_data['Date completed']).dt.to_period('M')
# Average ECO Released Monthly
monthly_eco_counts = eco_data.groupby('YearMonth')['Change#'].nunique()
avg_eco_monthly = monthly_eco_counts.mean() if not monthly_eco_counts.empty else 0
# Total ECOs Released
total_eco = eco_data['Change#'].nunique()
except Exception as e:
print(f"Error processing ECO data: {e}")
# Calculate theoretical fees and fees vs. theoretical
theoretical_fees = total_eco * 5000 / 1000 # Convert to $K
fees_vs_theoretical = (total_nre_fees / theoretical_fees * 100) if theoretical_fees > 0 else 0
# --- Generate HTML Text for Div ---
html_text = f"""
<div style="font-family: Arial, sans-serif; font-size: 14px; width: 1200px; padding: 10px;">
<p><b>Average NRE fees charged monthly:</b> ${avg_nre_fees_monthly:,.1f}K</p>
<p><b>Average NRE spent monthly:</b> ${avg_nre_spending_monthly:,.1f}K</p>
<p><b>Average ECO released monthly:</b> {avg_eco_monthly:.1f}</p>
<p><b>Quantity ECO released on the period:</b> {total_eco}</p>
<hr style="border: 1px solid lightgray;">
<p><b>Theoretical fees to be charged ($5K per ECO):</b> ${theoretical_fees:,.1f}K</p>
<p><b>Fees charged vs. theoretical:</b> {fees_vs_theoretical:.1f}%</p>
</div>
"""
# Create Bokeh Div for HTML text
text_div = Div(text=html_text, width=1200)
# Combine plot and text in a column layout
layout = column(p, text_div, sizing_mode='fixed')
return layout
#-----------------------------------------------------------------------------------------------------------
##################################################################
#///////////////////////////////////////////////////////////////
# Create INDICATORS
#///////////////////////////////////////////////////////////////
##################################################################
# Card 'Yearly metrics - comparison this year vs last year
##################################################################
# --> Comparison of cumulative metrics from the start of the current year up to today against the same period in the previous year
def calculate_yearly_metrics(df_Historic_dashboard_filtered):
today = pd.to_datetime('today')
current_year = today.year
last_year = current_year - 1
current_month = today.month
current_day = today.day
# Calculate start date for the current year and last year
start_of_current_year = pd.Timestamp(year=current_year, month=1, day=1)
start_of_last_year = pd.Timestamp(year=last_year, month=1, day=1)
end_of_last_year = pd.Timestamp(year=last_year, month=current_month, day=current_day)
# Filter and aggregate data for the current year up to today
df_Current_Year = df_Historic_dashboard_filtered[
(df_Historic_dashboard_filtered['Year'] == current_year) &
(df_Historic_dashboard_filtered['Invoice date'] <= today)
].agg({
'Qty Shipped': 'sum',
'Sales USD': 'sum'
}).to_dict()
# Filter and aggregate data for the last year up to the same date
df_Last_Year = df_Historic_dashboard_filtered[
(df_Historic_dashboard_filtered['Year'] == last_year) &
(df_Historic_dashboard_filtered['Invoice date'] <= end_of_last_year)
].agg({
'Qty Shipped': 'sum',
'Sales USD': 'sum'
}).to_dict()
# All values converted to integer
total_shipped_current = int(df_Current_Year['Qty Shipped'])
total_sales_current = int(df_Current_Year['Sales USD'])
total_shipped_last = int(df_Last_Year['Qty Shipped'])
total_sales_last = int(df_Last_Year['Sales USD'])
# Calculate percentage change
if total_shipped_last != 0:
pct_change_shipped = ((total_shipped_current - total_shipped_last) / total_shipped_last) * 100
else:
pct_change_shipped = float('inf') if total_shipped_current > 0 else float('-inf')
if total_sales_last != 0:
pct_change_sales = ((total_sales_current - total_sales_last) / total_sales_last) * 100
else:
pct_change_sales = float('inf') if total_sales_current > 0 else float('-inf')
return {
'total_shipped_current': total_shipped_current,
'total_sales_current': total_sales_current,
'total_shipped_last': total_shipped_last,
'total_sales_last': total_sales_last,
'pct_change_shipped': pct_change_shipped,
'pct_change_sales': pct_change_sales
}
def create_yearly_metrics_indicator(df_Historic_dashboard_filtered):
# Calculate the metrics
metrics = calculate_yearly_metrics(df_Historic_dashboard_filtered)
# Determine trend colors and arrows
trend_color_shipped = "green" if metrics['pct_change_shipped'] >= 0 else "red"
trend_arrow_shipped = "▲" if metrics['pct_change_shipped'] >= 0 else "▼"
trend_color_sales = "green" if metrics['pct_change_sales'] >= 0 else "red"
trend_arrow_sales = "▲" if metrics['pct_change_sales'] >= 0 else "▼"
# Create HTML content with trend information
html_content = f"""
<div style="font-size: 20px; font-family: Arial, sans-serif; padding: 10px; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;">
<h3 style="margin: 0; color: teal;"> Yearly comparison - {pd.to_datetime('today').year} to date vs. {pd.to_datetime('today').year - 1}</h3>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total Quantity Shipped YTD</strong> (vs. {pd.to_datetime('today').year - 1}): <span style="color: black;">{metrics['total_shipped_current']:,.0f}</span>
<span style="font-size: 14px; color: {trend_color_shipped};">({trend_arrow_shipped} {metrics['pct_change_shipped']:+,.1f}%)</span>
</p>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total realized Sales YTD</strong> (vs. {pd.to_datetime('today').year - 1}): <span style="color: black;">${metrics['total_sales_current']:,.0f}</span>
<span style="font-size: 14px; color: {trend_color_sales};">({trend_arrow_sales} {metrics['pct_change_sales']:+,.1f}%)</span>
</p>
</div>
"""
# Create layout with indicators and HTML content
layout = pn.Column(
pn.pane.HTML(html_content, sizing_mode='stretch_width'),
sizing_mode='stretch_width'
)
return layout
##################################################################
# Card 'Since Inception - Beginning of the project'
##################################################################
# update 09/12 --> Include 'Total backlog Sales (USD)' & 'Total past due Sales (USD)'
# This 2 new variables should come from the datafram 'df_Backlog_dashboard' and be calculated based on the 'Requested Date' (because the 'Due Date' has been changed for some PN, the 'Requested Date' the real due date.
#------------------------------------------------------------------------------------------------------------------
# 03/21 - Update the card with df_Snapshot_table column 'IDD Corrected Margin [%]' and 'IDD AVG realized Margin [%]'
#------------------------------------------------------------------------------------------------------------------
# Include 4 new KPI lines in the card:
# 'AVG Realized Margin' based on 'IDD AVG realized Margin [%]' for the given program
# 'Gross Profit Realized' based on 'IDD AVG realized Margin [%]' * Sum of 'Sales USD' df_Historic_dashboard_filtered from the given program
# 'Current Margin (Corrected based on labor)' based on 'IDD Corrected Margin [%]' for the given program
# 'Gross Profit (Corrected based on labor)' based on 'IDD Corrected Margin [%]' * Sum of 'Sales USD' for the given program
#------------------------------------------------------------------------------------------------------------------
# The overall margin for each program is based on the number of units shipped for each top-level item within the program.
# Thus, is an weighted average of the margins of individual items, where the weights are the quantities shipped.
#------------------------------------------------------------------------------------------------------------------
def clean_and_convert_columns(df, columns_to_clean):
"""
Clean specified columns by removing '%' and '$' symbols and converting them to numeric values.
Parameters:
df (pd.DataFrame): The DataFrame containing the columns to clean.
columns_to_clean (list): List of column names to clean.
Returns:
pd.DataFrame: The DataFrame with cleaned and converted columns.
"""
for column in columns_to_clean:
if column in df.columns:
# Remove '%' and '$' symbols and convert to numeric
df[column] = df[column].replace({'%': '', r'\$': ''}, regex=True)
df[column] = pd.to_numeric(df[column], errors='coerce') # Convert to numeric, set invalid values to NaN
else:
print(f"Warning: Column '{column}' not found in DataFrame.")
return df
# List of columns to clean (replace with your actual column names)
columns_to_clean = [
'IDD AVG realized Margin [%]',
'IDD Corrected Margin [%]',
# Add other columns as needed
]
# df_Snapshot_KPI datafram
df_Snapshot_KPI = df_Snapshot.copy()
# Clean and convert columns in df_Snapshot_KPI
df_Snapshot_KPI = clean_and_convert_columns(df_Snapshot_KPI, columns_to_clean)
#03/31WIP
def calculate_since_inception_metrics(df_Historic_dashboard_filtered, df_Snapshot_KPI):
"""
Calculate cumulative metrics since inception for the selected program.
Parameters:
df_Historic_dashboard_filtered (pd.DataFrame): Filtered DataFrame for the selected program (existing metrics).
df_Snapshot_KPI (pd.DataFrame): Snapshot data for the selected program (new metrics).
Returns:
dict: Dictionary containing cumulative metrics.
"""
# Display datafram
#display(df_Historic_dashboard_filtered.head())
# Existing metrics (from df_Historic_dashboard_filtered)
total_shipped = int(df_Historic_dashboard_filtered['Qty Shipped'].sum())
total_sales = int(df_Historic_dashboard_filtered['Sales USD'].sum())
# Calculate avg_realized_margin using 'IDD Marge Standard' and 'Quantity'
# Total margin in USD = sum of (IDD Marge Standard * Quantity) for each row
total_margin_usd = df_Historic_dashboard_filtered['IDD Marge Standard'].sum()
# Average realized margin (%) = (total margin USD / total sales) * 100
avg_realized_margin = (total_margin_usd / total_sales) * 100 if total_sales != 0 else 0
# Gross profit realized = total margin in USD
gross_profit_realized = total_margin_usd
# From df_Snapshot_KPI
current_margin_corrected = df_Snapshot_KPI['IDD Corrected Margin [%]'].mean() # Current Margin (Corrected based on labor)
gross_profit_corrected = current_margin_corrected * total_sales / 100 # Gross Profit (Corrected based on labor)
# Return metrics as a dictionary
return {
'total_shipped': total_shipped,
'total_sales': total_sales,
'avg_realized_margin': avg_realized_margin, # Updated metric
'gross_profit_realized': gross_profit_realized, # Updated metric
'current_margin_corrected': current_margin_corrected,
'gross_profit_corrected': gross_profit_corrected
}
# Updated 03/28
def create_since_inception_indicator(df_Historic_dashboard_filtered, df_Snapshot_KPI):
"""
Create a card displaying cumulative KPIs since inception, including program name.
"""
# Calculate cumulative metrics
metrics = calculate_since_inception_metrics(df_Historic_dashboard_filtered, df_Snapshot_KPI)
# Get the program name
program_name = df_Snapshot_KPI['Program'].iloc[0] if not df_Snapshot_KPI['Program'].empty else "Unknown Program"
# Special handling for 1stTB program
if program_name == '1stTB':
# Calculate total fees
total_nre_fee = int(df_Historic_dashboard_filtered['NRE Fee'].sum())
total_fai_fee = int(df_Historic_dashboard_filtered['FAI Fee'].sum())
total_fees = total_nre_fee + total_fai_fee
html_content = f"""
<div style="font-size: 20px; font-family: Arial, sans-serif; padding: 10px; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;">
<h3 style="margin: 0; color: teal;"> 1 year span ({span_report_historic_dashboard}) - {program_name}</h3>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total Shipped:</strong> {metrics['total_shipped']:,.0f} units
</p>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total Sales:</strong> ${metrics['total_sales'] / 1000:,.1f}K
</p>
<hr style="border: none; height: 0.5px; background-color: #ddd; margin: 10px 0;">
<p style="margin: 10px 0; font-size: 16px;">
<strong>Realized Gross Margin</strong> (ERP based):
<span style="color: {'green' if metrics['avg_realized_margin'] >= 0 else 'red'};">{metrics['avg_realized_margin']:.2f}%</span>
</p>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Realized Gross Profit</strong> (ERP based):
<span style="color: {'green' if metrics['gross_profit_realized'] >= 0 else 'red'};">${metrics['gross_profit_realized'] / 1000:,.1f}K</span>
</p>
</div>
"""
else:
# Original content for other programs
html_content = f"""
<div style="font-size: 20px; font-family: Arial, sans-serif; padding: 10px; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;">
<h3 style="margin: 0; color: teal;">Cumulative KPIs Since Inception - {program_name}</h3>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total Shipped:</strong> {metrics['total_shipped']:,.0f} units
</p>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total Sales:</strong> ${metrics['total_sales'] / 1000:,.1f}K
</p>
<hr style="border: none; height: 0.5px; background-color: #ddd; margin: 10px 0;">
<p style="margin: 10px 0; font-size: 16px;">
<strong>Realized Gross Margin</strong> (ERP based):
<span style="color: {'green' if metrics['avg_realized_margin'] >= 0 else 'red'};">{metrics['avg_realized_margin']:.2f}%</span>
</p>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Realized Gross Profit</strong> (ERP based):
<span style="color: {'green' if metrics['gross_profit_realized'] >= 0 else 'red'};">${metrics['gross_profit_realized'] / 1000:,.1f}K</span>
</p>
</div>
"""
# Create a layout with the HTML content
layout = pn.Column(
pn.pane.HTML(html_content, sizing_mode='stretch_width'),
sizing_mode='stretch_width'
)
return layout
def create_note_since_inception(df_Snapshot_KPI):
"""
Create an HTML note explaining the calculation of profit and margin for the since-inception indicator,
including the program name and unique PN count.
Returns:
str: HTML content for the note.
"""
# Get the program name (assuming 'Program' is a column in df_Snapshot_KPI)
program_name = df_Snapshot_KPI['Program'].iloc[0] if not df_Snapshot_KPI['Program'].empty else "Unknown Program"
# Count unique PN
unique_pn_count = df_Snapshot_KPI['IDD Top Level'].nunique()
note = f"""
<div style="
font-size: 12px;
font-family: Arial, sans-serif;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
width: 1310px;
height: 120px;
box-sizing: border-box;
overflow: auto;
line-height: 1.1; <!-- Reduced line height -->
position: relative; <!-- Required for arrow positioning -->
">
<p style="margin: 10px 0;">
▷ The overall profit and margin for each Program are calculated as a <b>weighted average</b> of the individual top-level margins within the program. The weights are based on the <b>number of units shipped</b> for each top-level item.
</p>
<p style="margin: 10px 0; padding-left: 20px;"> <!-- Indentation using padding -->
➥ <b>Realized</b> profit and margin are derived from the <b>ERP turnover report</b>, which captures price changes over time. However, this calculation does not account for differences between <b>actual labor time</b> and the <b>standard labor time</b> reflected in the pricing.
</p>
<p style="margin: 10px 0; padding-left: 20px;"> <!-- Indentation using padding -->
➥ <b>Corrected</b> profit and margin incorporate data from <b>labor reports</b> for each top-level item. This adjustment provides a <b>more accurate financial picture</b> by accounting for actual labor time variances.
</p>
<p style="margin: 10px 0; padding-left: 20px;">
➥ <b>{program_name}:</b> {unique_pn_count} unique top-level PN accounted for within the calculation.
</p>
</div>
"""
return note
#------------------------------------------------------------------------------------------------------------------
# 03/25 - Create New graph Monthly Sales and Fees
#------------------------------------------------------------------------------------------------------------------
#04/01 to include margin
def create_monthly_sales_fees_graph(df_Historic_dashboard_filtered, year_color_map, selected_program):
"""
Creates a stacked bar chart showing monthly sales and fees with a secondary axis for % margin.
Args:
df_Historic_dashboard_filtered: Filtered historical data
year_color_map: Dictionary mapping years to colors
Returns:
Bokeh figure object: The created visualization figure
"""
# Data Preparation
df = df_Historic_dashboard_filtered.copy()
df['YearMonth'] = df['Invoice date'].dt.to_period('M')
# Group by YearMonth and calculate metrics
monthly_data = df.groupby('YearMonth').agg({
'Sales USD': 'sum',
'NRE Fee': 'sum',
'FAI Fee': 'sum',
'Qty Shipped': 'sum',
'IDD Marge Standard': 'sum'
}).reset_index()
# Calculate total fees and convert to $K
monthly_data['Total Fees'] = monthly_data['NRE Fee'] + monthly_data['FAI Fee']
monthly_data['Sales_K'] = monthly_data['Sales USD']
monthly_data['Fees_K'] = monthly_data['Total Fees']
monthly_data['Total_K'] = monthly_data['Sales_K'] + monthly_data['Fees_K']
# Calculate margin percentage (margin/sales) in decimal form
monthly_data['Margin_Pct'] = monthly_data['IDD Marge Standard'] / monthly_data['Sales USD']
# Create display columns
monthly_data['Date'] = monthly_data['YearMonth'].dt.to_timestamp()
monthly_data['Month_Year'] = monthly_data['Date'].dt.strftime('%b %Y')
monthly_data['Year'] = monthly_data['Date'].dt.year
monthly_data['Color'] = monthly_data['Year'].map(year_color_map).fillna('gray')
monthly_data['Sales_Display'] = monthly_data['Sales_K'] / 1000
monthly_data['Fees_Display'] = monthly_data['Fees_K'] / 1000
monthly_data['Total_Display'] = monthly_data['Total_K'] / 1000
# Add a column for margin circle colors
monthly_data['Margin_Circle_Color'] = monthly_data['Margin_Pct'].apply(lambda x: '#82c372' if x >= 0 else '#ec3a42')
# Create figure with SVG output backend
p = figure(
x_range=monthly_data['Month_Year'],
title=f"Monthly Sales and Fees Breakdown - Program: {selected_program}",
width=1200,
height=650,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='right',
x_axis_label='Month',
y_axis_label='Revenue ($K)',
sizing_mode='fixed',
output_backend='svg'
)
# Set the title text color
#p.title.text_color = 'black'
# X-axis font size 04/02
p.xaxis.major_label_text_font_size = "12pt"
p.xaxis.major_label_text_font_style = "bold"
# Title text font
p.title.text_font_size = "14pt"
# Customize grid
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.xgrid.visible = False
# Create ColumnDataSource
source = ColumnDataSource(monthly_data)
# Create legend items
sales_legend_items = []
fees_legend_items = []
renderers = []
# Set range for primary y-axis (Revenue in $K) with more room at the top
p.y_range = Range1d(0, monthly_data['Total_K'].max() * 1.5) #150% primary Y-Axis (Sales $K)
# Add secondary y-axis for margin percentage with more room at the top
margin_min = monthly_data['Margin_Pct'].min() * 1
margin_max = monthly_data['Margin_Pct'].max() * 2 #200% secondary Y-Axis (Margin %)
p.extra_y_ranges = {"margin": Range1d(start=margin_min, end=margin_max)}
margin_axis = LinearAxis(
y_range_name="margin",
axis_label="Margin (%)",
formatter=NumeralTickFormatter(format="0%"),
major_label_text_color="black",
axis_label_text_color="black"
)
p.add_layout(margin_axis, 'right')
# Visualization code - bars
for year in sorted(monthly_data['Year'].unique()):
year_data = monthly_data[monthly_data['Year'] == year]
year_source = ColumnDataSource(year_data)
color = year_color_map.get(year, 'gray')
# Sales bars (solid color)
sales_bar = p.vbar(
x='Month_Year',
width=0.8,
bottom=0,
top='Sales_K',
source=year_source,
color=color,
alpha=1.0,
name=f"sales_{year}"
)
sales_legend_items.append((f"{year}", [sales_bar]))
renderers.append(sales_bar)
# Fees bars (with pattern)
fees_bar = p.vbar(
x='Month_Year',
width=0.8,
bottom='Sales_K',
top='Total_K',
source=year_source,
color=color,
alpha=0.3,
hatch_pattern="/",
hatch_color=color,
hatch_alpha=0.7,
hatch_weight=0.8,
name=f"fees_{year}"
)
fees_legend_items.append((f"{year}", [fees_bar]))
renderers.append(fees_bar)
# Prepare data for segmented margin line
xs = []
ys = []
colors = []
for i in range(len(monthly_data) - 1):
xs.append([monthly_data['Month_Year'].iloc[i], monthly_data['Month_Year'].iloc[i + 1]])
ys.append([monthly_data['Margin_Pct'].iloc[i], monthly_data['Margin_Pct'].iloc[i + 1]])
colors.append('#82c372' if monthly_data['Margin_Pct'].iloc[i] >= 0 else '#ec3a42')
# Add visible segmented margin line
margin_line = p.multi_line(
xs=xs,
ys=ys,
line_color=colors,
line_width=2,
line_dash='dashed',
y_range_name="margin"
)
# Add empty circles for each margin data point with conditional coloring
margin_circles = p.circle(
x='Month_Year',
y='Margin_Pct',
source=source,
y_range_name="margin",
size=8,
fill_alpha=0,
line_color='Margin_Circle_Color',
line_width=1.5
)
# Create dummy lines for the margin legend
positive_margin_dummy = p.line(
x=[monthly_data['Month_Year'].iloc[0]],
y=[monthly_data['Margin_Pct'].iloc[0]],
line_color='#82c372',
line_width=2,
line_dash='dashed',
y_range_name="margin"
)
negative_margin_dummy = p.line(
x=[monthly_data['Month_Year'].iloc[0]],
y=[monthly_data['Margin_Pct'].iloc[0]],
line_color='#ec3a42',
line_width=2,
line_dash='dashed',
y_range_name="margin"
)
# Add invisible line for hover
invisible_margin_line = p.line(
x='Month_Year',
y='Margin_Pct',
source=source,
y_range_name="margin",
line_width=2,
line_alpha=0,
hover_line_color='black',
hover_alpha=0.5
)
# Update hover tool
hover = HoverTool(
tooltips=[
("Period", "@Month_Year"),
("Sales", "@Sales_Display{$0,0.0}K"),
("Fees", "@Fees_Display{$0,0.0}K"),
("Total", "@Total_Display{$0,0.0}K"),
("Margin", "@Margin_Pct{0.0%}"),
("Quantity Shipped", "@{Qty Shipped}{0,0}")
],
mode='vline',
renderers=[invisible_margin_line]
)
p.add_tools(hover)
# Remove Bokeh logo
p.toolbar.logo = None
# Create legends
sales_legend = Legend(
items=sales_legend_items,
location=(50, 400),
title="Sales",
title_text_font_size="10pt",
label_text_font_size="10pt",
glyph_height=20,
glyph_width=20,
spacing=5,
background_fill_color="white",
background_fill_alpha=0.8,
border_line_color=None,
border_line_width=1
)
fees_legend = Legend(
items=fees_legend_items,
location=(150, 400),
title="Fees",
title_text_font_size="10pt",
label_text_font_size="10pt",
glyph_height=20,
glyph_width=20,
spacing=5,
background_fill_color="white",
background_fill_alpha=0.8,
border_line_color=None,
border_line_width=1
)
# Add a separate legend for margin lines
margin_legend_items = [
("Gross Margin % (> 0)", [positive_margin_dummy]),
("Gross Margin % (< 0)", [negative_margin_dummy])
]
margin_legend = Legend(
items=margin_legend_items,
location=(250, 400),
title="Margin (ERP based)",
title_text_font_size="10pt",
label_text_font_size="10pt",
glyph_height=20,
glyph_width=20,
spacing=5,
background_fill_color="white",
background_fill_alpha=0.8,
border_line_color=None,
border_line_width=1
)
p.add_layout(sales_legend)
p.add_layout(fees_legend)
p.add_layout(margin_legend)
# Final styling
p.xaxis.major_label_orientation = 1.2
p.yaxis.formatter = NumeralTickFormatter(format="$0a")
p.yaxis[1].formatter = NumeralTickFormatter(format="0%")
p.legend.click_policy = "hide"
return p
#------------------------------------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------------------------------------
# 03/27WIP - Define the Function to Select Top PNs
#------------------------------------------------------------------------------------------------------------------
# Updated 04/09
def get_top_critical_assemblies_by_value(df_Backlog_filtered, comparison_date, selected_program, num_pns=5):
"""
Calculate top critical assemblies by backlog dollar value.
Parameters:
df_Backlog_filtered (pd.DataFrame): Filtered DataFrame with backlog data.
comparison_date (datetime): Date for determining past-due status.
selected_program (str): Program name to determine index column.
num_pns (int): Number of top assemblies to return (default: 5).
Returns:
pd.DataFrame: DataFrame with index column, metrics, and additional columns for Phase 4-5.
"""
try:
df = df_Backlog_filtered.copy()
index_column = 'Pty Indice' if selected_program == 'Phase 4-5' else 'IDD Top Level'
# Verify index column exists
if index_column not in df.columns:
raise ValueError(f"Required column '{index_column}' not found in input DataFrame. Available columns: {list(df.columns)}")
# Ensure Due Date is datetime64[ns]
df['Due Date'] = pd.to_datetime(df['Due Date'], errors='coerce')
df['Backlog Value'] = df['Currency net amount']
# Filter past-due items
past_due_df = df[df['Due Date'] < comparison_date]
if past_due_df.empty:
print(f"No past-due assemblies found. Due dates range: {df['Due Date'].min()} to {df['Due Date'].max()}")
return pd.DataFrame({
index_column: [],
'Past Due Value': [],
'Total Value': [],
'Percentage': [],
'IDD Top Level': [] if selected_program == 'Phase 4-5' else [],
'SEDA Top Level': [] if selected_program == 'Phase 4-5' else []
})
# Aggregate past-due values
past_due_summary = (past_due_df.groupby(index_column, as_index=False)
.agg({'Backlog Value': 'sum'})
.rename(columns={'Backlog Value': 'Past Due Value'}))
# Get top N by past-due value
past_due_summary = past_due_summary.nlargest(num_pns, 'Past Due Value')
# Aggregate total values
total_summary = (df.groupby(index_column, as_index=False)
.agg({'Backlog Value': 'sum'})
.rename(columns={'Backlog Value': 'Total Value'}))
# For Phase 4-5, get IDD Top Level and SEDA Top Level
if selected_program == 'Phase 4-5':
# Get unique mappings of Pty Indice to IDD Top Level and SEDA Top Level
mapping_df = df[[index_column, 'IDD Top Level', 'SEDA Top Level']].drop_duplicates(subset=[index_column])
else:
mapping_df = pd.DataFrame({index_column: past_due_summary[index_column]})
# Merge to combine past-due and total values
summary_df = pd.merge(past_due_summary, total_summary, on=index_column, how='left')
# Merge the IDD Top Level and SEDA Top Level mappings
if selected_program == 'Phase 4-5':
summary_df = pd.merge(summary_df, mapping_df, on=index_column, how='left')
# Fill any missing values with 'N/A'
summary_df['IDD Top Level'] = summary_df['IDD Top Level'].fillna('N/A')
summary_df['SEDA Top Level'] = summary_df['SEDA Top Level'].fillna('N/A')
# Calculate percentage of total PAST DUE backlog
total_past_due_value = past_due_df['Backlog Value'].sum()
if total_past_due_value > 0:
summary_df['Percentage'] = (summary_df['Past Due Value'] / total_past_due_value * 100).round(1)
else:
summary_df['Percentage'] = 0.0
return summary_df
except Exception as e:
print(f"Error in get_top_critical_assemblies_by_value: {str(e)}")
return pd.DataFrame({
index_column: [],
'Past Due Value': [],
'Total Value': [],
'Percentage': [],
'IDD Top Level': [] if selected_program == 'Phase 4-5' else [],
'SEDA Top Level': [] if selected_program == 'Phase 4-5' else []
})
# Updated 04/09
def create_critical_assemblies_card(df_Backlog_filtered, file_date, selected_program, num_pns=5):
try:
# Convert file_date to datetime for comparison
comparison_date = pd.to_datetime(file_date)
# Get metrics for top assemblies
metrics_df = get_top_critical_assemblies_by_value(
df_Backlog_filtered,
comparison_date,
selected_program,
num_pns
)
if metrics_df.empty:
return pn.Column(
pn.pane.Alert("No past-due assemblies found", alert_type="warning"),
sizing_mode='stretch_width'
)
# Calculate totals
formatted_date = comparison_date.strftime('%m-%d-%Y')
total_past_due_value = metrics_df['Past Due Value'].sum()
total_value = metrics_df['Total Value'].sum()
# Filter for past due items
past_due_mask = pd.to_datetime(df_Backlog_filtered['Due Date']) < comparison_date
total_past_due_backlog = df_Backlog_filtered.loc[past_due_mask, 'Currency net amount'].sum()
# Create HTML header without inner border
html_content = f"""
<div style="font-family: Arial, sans-serif; padding: 15px;">
<h3 style="margin: 0 0 5px 0; color: teal; font-size: 22px;">
Top {len(metrics_df)} Assemblies by $ value</h3>
<p style="margin: 5px 0; font-size: 14px;">
<strong>Related Backlog [{formatted_date}]:</strong><br>
<span style="margin-left: 20px;">• <strong>Past Due:</strong> ${total_past_due_value/1_000:,.0f}K ({(total_past_due_value/total_past_due_backlog*100 if total_past_due_backlog > 0 else 0):.1f}% of total past due)</span><br>
<span style="margin-left: 20px;">• <strong>Total Backlog:</strong> ${total_value/1_000:,.0f}K</span>
</p>
</div>
"""
# Determine column name based on selected_program
index_column = 'Pty Indice' if selected_program == 'Phase 4-5' else 'IDD Top Level'
# Ensure that index_column is in the DataFrame before renaming
if index_column not in metrics_df.columns:
available_columns = list(metrics_df.columns)
raise ValueError(f"Expected column '{index_column}' not found in metrics_df. Available columns: {available_columns}")
# Prepare chart data (values in original units)
chart_data = metrics_df.rename(columns={
index_column: 'PN',
'Past Due Value': 'Past_Due',
'Total Value': 'Total'
})
chart_data['Percent'] = metrics_df['Percentage'].astype(str) + '%'
# Prepare table data with additional columns for hover when Phase 4-5
table_data = chart_data[['PN', 'Past_Due', 'Total']].copy()
table_data['% Past Due'] = (chart_data['Past_Due'] / total_past_due_backlog * 100).round(1)
# Add IDD Top Level and SEDA Top Level columns if Phase 4-5
if selected_program == 'Phase 4-5':
table_data['IDD Top Level'] = metrics_df['IDD Top Level']
table_data['SEDA Top Level'] = metrics_df['SEDA Top Level']
table_data.rename(columns={
'PN': 'Pty' if selected_program == 'Phase 4-5' else 'IDD PN',
'Past_Due': 'Past Due ($K)',
'Total': 'Total Backlog ($K)'
}, inplace=True)
# Format numeric columns
table_data['Past Due ($K)'] = table_data['Past Due ($K)'].apply(lambda x: f"${x/1_000:,.0f}K")
table_data['Total Backlog ($K)'] = table_data['Total Backlog ($K)'].apply(lambda x: f"${x/1_000:,.0f}K")
table_data['% Past Due'] = table_data['% Past Due'].apply(lambda x: f"{x:.1f}%")
# Create table with proper styling
if selected_program == 'Phase 4-5':
table_html = """
<style>
.custom-table {
text-align: center;
font-size: 12px;
margin: 0 auto;
border-collapse: collapse;
font-family: Arial, sans-serif;
}
.custom-table th {
padding: 8px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.custom-table td {
padding: 8px;
border-bottom: 1px solid #eee;
}
.custom-table td:hover {
background-color: #f9f9f9;
cursor: pointer;
}
</style>
<table class='custom-table'>
"""
table_html += '<tr>' + ''.join(f'<th>{col}</th>' for col in table_data.columns[:4]) + '</tr>'
for _, row in table_data.iterrows():
tooltip = f'IDD Top Level: {row["IDD Top Level"]}\nSEDA Top Level: {row["SEDA Top Level"]}'
table_html += '<tr>'
table_html += f'<td title="{tooltip}">{row[table_data.columns[0]]}</td>'
table_html += ''.join(f'<td>{row[col]}</td>' for col in table_data.columns[1:4])
table_html += '</tr>'
table_html += '</table>'
table_pane = pn.pane.HTML(table_html, sizing_mode='stretch_width')
else:
table_styles = {
'text-align': 'center',
'font-size': '12px',
'margin': '0 auto'
}
table_pane = pn.pane.DataFrame(table_data.iloc[:, :4],
index=False,
sizing_mode='stretch_width',
styles=table_styles)
# Create Bokeh chart with K-formatted Y-axis
source = ColumnDataSource(chart_data)
max_value = max(chart_data['Total'].max(), chart_data['Past_Due'].max())
y_upper = max_value * 1.4
p = figure(
x_range=chart_data['PN'].tolist(),
height=300,
width=400,
title=f"Top {min(num_pns, len(metrics_df))} Assemblies by Backlog Value",
tools="hover,reset",
toolbar_location=None,
background_fill_color='white',
y_range=(0, y_upper)
)
p.ygrid.grid_line_dash = 'dashed'
# Add bars
past_due = p.vbar(
x=dodge('PN', -0.1, range=p.x_range),
top='Past_Due',
width=0.2,
source=source,
fill_color='#e15759',
legend_label='$ Past Due',
alpha=0.8
)
total = p.vbar(
x=dodge('PN', 0.1, range=p.x_range),
top='Total',
width=0.2,
source=source,
fill_color='#4e79a7',
legend_label='$ Total',
alpha=0.8
)
# Configure hover tool with conditional label
hover_label = "Pty" if selected_program == 'Phase 4-5' else "IDD"
p.add_tools(HoverTool(
tooltips=[
(hover_label, "@PN"),
("Past Due", "@Past_Due{$0,0}"),
("Total", "@Total{$0,0}"),
("% of Total Past Due", "@Percent")
],
renderers=[past_due, total]
))
# Format Y-axis to show K suffix
p.yaxis.formatter = NumeralTickFormatter(format="$0,0K")
p.yaxis.axis_label = "Backlog Value ($K)"
p.xaxis.major_label_orientation = 45
p.xgrid.grid_line_color = None
p.legend.location = "top_left"
p.legend.click_policy = "hide"
p.legend.orientation = "horizontal"
# Create the final layout with outer border only
return pn.Column(
pn.pane.HTML(html_content, sizing_mode='stretch_width'),
pn.Spacer(height=20),
table_pane,
pn.Spacer(height=40),
pn.pane.Bokeh(p, sizing_mode='stretch_width'),
styles={
'border': '1px solid #e0e0e0',
'border-radius': '5px',
'padding': '10px'
}
)
except Exception as e:
error_msg = f"Error displaying critical assemblies: {str(e)}"
print(error_msg)
return pn.Column(
pn.pane.Alert(error_msg, alert_type="danger"),
sizing_mode='stretch_width'
)
#------------------------------------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------------------------------------
# 04/01 - Create new Graph 'Monthly Engineering Hours per Team' from df_SciformaReport_Team
#------------------------------------------------------------------------------------------------------------------
df_SciformaReport_Team = pd.read_excel(input_file_formatted, sheet_name='SciformaReport_Team', index_col=False)
# Keep the years in the usual color scheme define in year_color_map and use a pattern for each function define in function_patterns
# Second step to be included later (data is missing currently): Create 2 bars for each month 'Planned' vs 'Realized' in the same pattern an colors with 'Planned' in alpha 0.5
#Combine 'Program Manager' and 'Manager' under 'Manager'
df_SciformaReport_Team['Function'] = df_SciformaReport_Team['Function'].replace({'Program Manager': 'Manager'})
#print('df_SciformaReport_Team:')
#display(df_SciformaReport_Team)
#------------------------------------------------------------------------------------------------------------------
# 04/07
def create_monthly_engineering_hours_graph(df_SciformaReport_Team_filtered, selected_program):
# Define pastel colors for each team
team_colors = {
"Mechanical Engineer": "#debbd4", #
"Electrical Engineer": "#B0E0E6", # Powder Blue
"System Engineer": "#98FB98", # Pale Green
"Mechanical Drafter": "#DDA0DD", # Plum
"Sustaining Engineer": "#93c7b2", #
"Manufacturing Engineer": "#E6E6FA",# Lavender
"Lightplate Engineer": "#FFA07A", # Light Salmon
"Project Engineer": "#B0C4DE", # Light Steel Blue
"Manager": "#F5DEB3" # Wheat
}
# Data Preparation
df = df_SciformaReport_Team_filtered.copy()
# Identify the month columns (all columns that look like 'YYYY-MM')
month_columns = [col for col in df.columns if col.startswith(('2024', '2025'))]
if not month_columns: # Handle case where no month columns are found
month_columns = []
month_columns = sorted(month_columns, key=lambda x: int(x.split('-')[0]), reverse=False)
# Check if required columns exist
if not all(col in df.columns for col in ['Program', 'Function']) or not month_columns:
p = figure(
x_range=["No Data"],
title=f"Monthly Engineering Hours per Team - Program: {selected_program}",
width=1200,
height=650,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='right',
x_axis_label='Month',
y_axis_label='Engineering Hours',
output_backend='svg'
)
p.text(
x=[0],
y=[50],
text=["Invalid or missing data in DataFrame"],
text_align="center",
text_font_size="12pt"
)
p.y_range = Range1d(0, 100)
p.xgrid.visible = False
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.toolbar.logo = None
return p
# Reshape the DataFrame
df = df.melt(
id_vars=['Program', 'Function'],
value_vars=month_columns,
var_name='Month',
value_name='Hours'
)
# Convert 'Month' to datetime format
df['Month'] = pd.to_datetime(df['Month'], format='%Y-%m')
df['YearMonth'] = df['Month'].dt.to_period('M')
# Aggregate data
monthly_data = df.groupby(['YearMonth', 'Function', 'Month'])['Hours'].sum().reset_index()
# Generate display values
monthly_data['Month_Year'] = monthly_data['Month'].dt.strftime('%b %Y')
# Assign colors by team
monthly_data['Color'] = monthly_data['Function'].map(lambda x: team_colors.get(x, '#D3D3D3')) # Default to light gray
monthly_data['BarAlpha'] = monthly_data['Hours'].apply(lambda x: 0.7 if x > 0 else 0.0)
# Sort data by month
monthly_data = monthly_data.sort_values('Month')
# Handle empty data case
unique_functions = sorted(monthly_data['Function'].unique())
if len(unique_functions) == 0:
p = figure(
x_range=["No Data"],
title=f"Monthly Engineering Hours per Team - Program: {selected_program}",
width=1200,
height=650,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='right',
x_axis_label='Month',
y_axis_label='Engineering Hours',
output_backend='svg'
)
p.text(
x=[0],
y=[50],
text=["No engineering hours data available for the selected filters"],
text_align="center",
text_font_size="12pt"
)
p.y_range = Range1d(0, 100)
p.xgrid.visible = False
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.toolbar.logo = None
return p
# Create ColumnDataSource
source = ColumnDataSource(monthly_data)
# Unique months for x-axis
unique_months = sorted(monthly_data['Month_Year'].unique(),
key=lambda x: pd.to_datetime(x, format='%b %Y'))
# Create figure
p = figure(
x_range=unique_months,
title=f"Monthly Engineering Hours per Team - Program: {selected_program}",
width=1200,
height=650,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='right',
x_axis_label='Month',
y_axis_label='Engineering Hours',
output_backend='svg'
)
# Set grid and y-range
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.xgrid.visible = False
# X-axis font size
p.xaxis.major_label_text_font_size = "12pt"
p.xaxis.major_label_text_font_style = "bold"
# Title text font
p.title.text_font_size = "14pt"
if not monthly_data.empty:
max_hours = monthly_data['Hours'].max()
p.y_range = Range1d(0, max_hours * 1.3)
else:
p.y_range = Range1d(0, 100)
# Calculate bar width and offsets
bar_width = 0.8 / len(unique_functions)
offsets = {func: (i - (len(unique_functions) - 1) / 2) * bar_width for i, func in enumerate(unique_functions)}
# Create bars
renderers = []
team_legend_items = []
used_functions = set()
for function in unique_functions:
func_data = monthly_data[monthly_data['Function'] == function]
func_source = ColumnDataSource(func_data)
color = team_colors.get(function, '#D3D3D3')
offset = offsets[function]
bar = p.vbar(
x=dodge('Month_Year', offset, range=p.x_range),
width=bar_width,
top='Hours',
source=func_source,
color=color,
alpha=0.7,
name=function
)
if function not in used_functions:
team_legend_items.append((function, [bar]))
used_functions.add(function)
renderers.append(bar)
# Add hover tool
hover = HoverTool(
tooltips=[
("Month", "@Month_Year"),
("Function", "@Function"),
("Hours", "@Hours{0,0}"),
],
mode='vline',
renderers=renderers
)
p.add_tools(hover)
# Create team legend only
team_legend = Legend(
items=team_legend_items,
title="Team",
location="top_center",
label_text_font_size="10pt",
background_fill_color=None,
background_fill_alpha=0.0,
border_line_color=None,
click_policy="hide",
ncols=3,
spacing=5,
margin=5
)
p.add_layout(team_legend, 'center')
p.min_border_left = 100
p.min_border_right = 100
p.xaxis.major_label_orientation = 1.2
p.yaxis.formatter = NumeralTickFormatter(format="0,0")
p.toolbar.logo = None
return p
#------------------------------------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------------------------------------
# 04/09 - Create new Graph 'create_monthly_engineering_cumulative_hours_graph' from df_SciformaReport and include the nubmer of ECO closed per month from df_AccessDB
#------------------------------------------------------------------------------------------------------------------
# 04/11 - Exclude 'Manager' hours from df_SciformaReport_Team
#--------------------------------------------------------------
#print('df_SciformaReport_Team')
#display(df_SciformaReport_Team)
#print('df_SciformaReport')
#display(df_SciformaReport)
#--------------------------------------------------------------
# Step 1: Calculate Manager hours from df_SciformaReport_Team
df_manager = df_SciformaReport_Team[df_SciformaReport_Team['Function'] == 'Manager'].copy()
month_columns = [col for col in df_manager.columns if col.startswith('202')]
if df_manager.empty or not month_columns:
print("Warning: No Manager data or valid month columns in df_SciformaReport_Team.")
df_manager_grouped = pd.DataFrame(columns=['Program', 'YearMonth', 'Manager_Hours'])
else:
# Melt Manager data
df_manager_melted = df_manager.melt(id_vars=['Program'], value_vars=month_columns,
var_name='YearMonth', value_name='Manager_Hours')
df_manager_melted['YearMonth'] = pd.to_datetime(df_manager_melted['YearMonth'], format='%Y-%m')
# Group by Program and YearMonth to sum Manager hours
df_manager_grouped = df_manager_melted.groupby(['Program', 'YearMonth'])['Manager_Hours'].sum().reset_index()
# Step 2: Prepare df_SciformaReport
df_hours = df_SciformaReport.copy()
month_columns = [col for col in df_hours.columns if col.startswith('202')]
if not month_columns:
print("Warning: No valid month columns in df_SciformaReport.")
elif df_hours[month_columns].isna().all().all():
print("Warning: df_SciformaReport contains only NaN values for hours.")
else:
# Melt df_SciformaReport to get hours per month
df_hours_melted = df_hours.melt(id_vars=['Program'], value_vars=month_columns,
var_name='YearMonth', value_name='Hours')
df_hours_melted['YearMonth'] = pd.to_datetime(df_hours_melted['YearMonth'], format='%Y-%m')
# Remove NaN hours and programs
df_hours_melted = df_hours_melted.dropna(subset=['Hours', 'Program'])
# Group by Program and YearMonth to sum hours (handles duplicates)
df_hours_grouped = df_hours_melted.groupby(['Program', 'YearMonth'])['Hours'].sum().reset_index()
# Step 3: Subtract Manager hours for corresponding Program
df_hours_grouped = df_hours_grouped.merge(df_manager_grouped, on=['Program', 'YearMonth'], how='left').fillna(0)
df_hours_grouped['Hours'] = df_hours_grouped['Hours'] - df_hours_grouped['Manager_Hours']
df_hours_grouped['Hours'] = df_hours_grouped['Hours'].clip(lower=0)
# Step 4: Pivot to wide format
df_hours_pivoted = df_hours_grouped.pivot(index='Program', columns='YearMonth', values='Hours').reset_index()
# Ensure all month columns exist
for col in month_columns:
col_dt = pd.to_datetime(col, format='%Y-%m')
if col_dt not in df_hours_pivoted.columns:
df_hours_pivoted[col_dt] = 0
# Rename columns to YYYY-MM format
df_hours_pivoted.columns = ['Program'] + [col.strftime('%Y-%m') if isinstance(col, pd.Timestamp) else col for col in df_hours_pivoted.columns[1:]]
# Reorder columns to match original
df_SciformaReport = df_hours_pivoted[['Program'] + month_columns]
# Ensure non-negative hours
df_SciformaReport[month_columns] = df_SciformaReport[month_columns].clip(lower=0)
# Warn if output is empty
if df_SciformaReport.empty or df_SciformaReport[month_columns].isna().all().all():
print("Warning: No valid hours remain after processing.")
#print('df_SciformaReport, excluding Manager')
#display(df_SciformaReport)
#----------------------------------------------------------------------------------------
# 04/14
def create_monthly_cumulative_engineering_hours_with_ECO(df_SciformaReport_filtered, df_AccessDB_filtered, selected_program, year_color_map):
# Prepare Engineering Hours data
df_hours = df_SciformaReport_filtered.copy()
month_columns = [col for col in df_hours.columns if col.startswith('202')]
df_hours_melted = df_hours.melt(id_vars=['Program'], value_vars=month_columns,
var_name='YearMonth', value_name='Hours')
df_hours_melted['YearMonth'] = pd.to_datetime(df_hours_melted['YearMonth'], format='%Y-%m')
df_hours_grouped = df_hours_melted.groupby('YearMonth').sum().reset_index()
df_hours_grouped['Month_Year'] = df_hours_grouped['YearMonth'].dt.strftime('%b %Y')
# Filter out months with no hours (empty data) from df_SciformaReport_filtered
df_hours_grouped = df_hours_grouped[df_hours_grouped['Hours'] > 0]
# Prepare ECO data
monthly_metrics = pd.DataFrame(columns=['Month_completed', 'Total_ECOs', 'Month_dt', 'Month_Year'])
if not df_AccessDB_filtered.empty and 'Month_completed' in df_AccessDB_filtered.columns:
try:
monthly_metrics = df_AccessDB_filtered.groupby('Month_completed').agg(
Total_ECOs=('ID', 'count')
).reset_index()
monthly_metrics['Month_dt'] = pd.to_datetime(monthly_metrics['Month_completed'], format='%b-%Y')
monthly_metrics = monthly_metrics.sort_values('Month_dt')
monthly_metrics['Month_Year'] = monthly_metrics['Month_dt'].dt.strftime('%b %Y')
except Exception as e:
print(f"Error processing ECO data: {e}")
# Merge the dataframes, keeping only months present in df_hours_grouped
df_combined = df_hours_grouped.merge(monthly_metrics[['Month_Year', 'Total_ECOs']],
on='Month_Year', how='left').fillna({'Total_ECOs': 0})
# Ensure YearMonth is still datetime after merge
df_combined['YearMonth'] = pd.to_datetime(df_combined['YearMonth'], errors='coerce')
# Sort the combined dataframe chronologically
df_combined['SortDate'] = pd.to_datetime(df_combined['Month_Year'], format='%b %Y')
df_combined = df_combined.sort_values('SortDate')
df_combined = df_combined.drop(columns=['SortDate']) # Clean up temporary column
# Handle case where df_combined is empty
if df_combined.empty:
print(f"No engineering hours data available for program: {selected_program}")
return pn.pane.Markdown(f"No engineering hours data for {selected_program}.")
# Apply existing year_color_map
df_combined['Year'] = df_combined['YearMonth'].dt.year
df_combined['Color'] = df_combined['Year'].map(year_color_map).fillna('gray')
# Calculate average hours
average_hours = df_combined['Hours'].mean()
# Create ColumnDataSource for the main data
source = ColumnDataSource(df_combined)
# Create a separate ColumnDataSource for ECO data, filtering out zero values
eco_data = df_combined[df_combined['Total_ECOs'] > 0]
eco_source = ColumnDataSource(eco_data)
# Create figure with sorted x_range, only including months with data
p = figure(
x_range=df_combined['Month_Year'].tolist(), # Only months with data
title=f"Cumulative Engineering Hours and ECO Released per Month - Program: {selected_program}",
width=1200,
height=700,
tools='pan,wheel_zoom,box_zoom,reset,save,hover',
toolbar_location='right',
y_range=(0, max(df_combined['Hours'].max(), 1) * 1.3), # Ensure y_range handles zero data
output_backend='svg'
)
# Primary axis (Hours) - Bars (render first)
bars = p.vbar(
x='Month_Year',
top='Hours',
width=0.4,
source=source,
color='Color',
y_range_name="default"
)
# Set the axis label for primary Y-axis
p.yaxis.axis_label = "Cumulative Hours Consumed"
# Create legend items for years (render bars for legend)
present_years = sorted(set(df_combined['Year'].unique()))
legend_years = [year for year in present_years if year in year_color_map]
renderers_by_year = {}
legend_items_years = []
for year in legend_years:
year_data = df_combined[df_combined['Year'] == year]
year_source = ColumnDataSource(year_data)
year_bar = p.vbar(
x='Month_Year',
top='Hours',
width=0.4,
source=year_source,
color=year_color_map[year],
visible=True
)
legend_items_years.append((str(year), [year_bar]))
renderers_by_year[year] = year_bar
# Average hours line (render after bars) - Use Span to span full width
average_line = Span(
location=average_hours,
dimension='width',
line_color='#7030A0',
line_dash=[6, 4], # Consistent dash pattern
line_width=2
)
p.add_layout(average_line)
# Proxy line for average hours in legend (not rendered on the plot)
avg_line_data = pd.DataFrame({
'x': [df_combined['Month_Year'].iloc[0], df_combined['Month_Year'].iloc[-1]],
'y': [0, 0] # y-value doesn't matter since it won't be rendered
})
avg_line_source = ColumnDataSource(avg_line_data)
avg_line_renderer = p.line(
x='x',
y='y',
source=avg_line_source,
line_color='#7030A0',
line_dash=[6, 4], # Match the dash pattern
line_width=2,
visible=True # Only used for legend, not rendered on plot
)
# Secondary axis (ECOs) - Only show for '1stTB' or 'Phase 4-5'
eco_line = None
eco_points = None
eco_legend_line = None
if selected_program in ('1stTB', 'Phase 4-5'):
max_ecos = df_combined['Total_ECOs'].max()
p.extra_y_ranges = {"changes": Range1d(start=0, end=max(10, max_ecos * 1.2))}
# Add secondary axis
secondary_axis = LinearAxis(
y_range_name="changes",
axis_label="ECOs Released",
axis_label_text_color="#4472C4",
)
p.add_layout(secondary_axis, 'right')
# Use full data source (no filtering) to ensure data is present
eco_source = ColumnDataSource(df_combined)
# ECO line
eco_line = p.line(
x='Month_Year',
y='Total_ECOs',
y_range_name="changes",
line_width=2,
color='#4472C4',
source=eco_source
)
# ECO points
eco_points = p.scatter(
x='Month_Year',
y='Total_ECOs',
y_range_name="changes",
size=10,
fill_color='white',
line_color='#4472C4',
line_width=2,
source=eco_source
)
# Proxy line for ECOs in legend
eco_legend_line = p.line(
x='Month_Year',
y='Total_ECOs',
y_range_name="changes",
line_width=2,
color='#4472C4',
source=eco_source,
visible=True
)
# Styling
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.xgrid.visible = False
p.xaxis.major_label_text_font_size = "12pt"
p.xaxis.major_label_text_font_style = "bold"
p.xaxis.major_label_orientation = 1.2
p.title.text_font_size = "14pt"
p.yaxis.axis_label_text_align = "center"
# Structure legend items with sections
final_legend_items = []
# "Years" section
final_legend_items.append(("Years", []))
final_legend_items.extend(legend_items_years)
# Spacer
final_legend_items.append((" " * 10, []))
# "Average hours" section
final_legend_items.append(("Average hours consumed on the period:", []))
final_legend_items.append((f'{average_hours:,.0f} Hours', [avg_line_renderer]))
# Spacer and "ECOs Released" section - only if program is '1stTB' or 'Phase 4-5'
if selected_program in ('1stTB', 'Phase 4-5'):
final_legend_items.append((" " * 10, []))
final_legend_items.append(("ECO Released", []))
final_legend_items.append(("", [eco_legend_line]))
# Add legend inside the plot
if final_legend_items:
legend = Legend(
items=final_legend_items,
label_text_font_size="10pt",
background_fill_alpha=0.7,
glyph_height=20,
glyph_width=20,
spacing=5,
orientation="horizontal",
click_policy="hide",
location=(50, 450),
border_line_color=None,
padding=5,
margin=10
)
p.add_layout(legend, 'center')
# Hover tool
hover = p.select_one(HoverTool)
hover.tooltips = [
("Month", "@Month_Year"),
("Cumulative Hours", "@Hours{0,0}"),
]
# Only include ECOs in hover if program is '1stTB' or 'Phase 4-5'
hover_renderers = [bars]
if selected_program in ('1stTB', 'Phase 4-5'):
hover.tooltips.append(("ECO Released", "@Total_ECOs"))
hover_renderers.extend([eco_line, eco_points]) # Include both line and points
hover.renderers = hover_renderers
p.toolbar.logo = None
return p
#------------------------------------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------------------------------------
# 04/02 - Create new Graph 'create_engineering_monthly_ECO_KPI_Graph' from df_AccessDB_filtered
#------------------------------------------------------------------------------------------------------------------
# update 04/15
def create_engineering_monthly_ECO_KPI_Graph(df_AccessDB_filtered, selected_program='1stTB'):
# Handle empty DataFrame
if df_AccessDB_filtered.empty:
print(f"No data available for program: {selected_program}")
return pn.pane.HTML("<h3>No ECO data available for 1stTB</h3>")
# Verify Month_completed column
if 'Month_completed' not in df_AccessDB_filtered.columns:
raise ValueError("Required column 'Month_completed' not found in df_AccessDB_filtered")
# Filter out NaN Month_completed
df_AccessDB_filtered = df_AccessDB_filtered[df_AccessDB_filtered['Month_completed'].notna()]
if df_AccessDB_filtered.empty:
print("No data after removing NaN Month_completed")
return pn.pane.HTML("<h3>No valid ECO data available for 1stTB</h3>")
# Data preparation
monthly_metrics = df_AccessDB_filtered.groupby('Month_completed').agg(
Avg_Completion_Time=('Completion Time', 'mean'),
Total_Jobs=('ID', 'count')
).reset_index()
# Convert to datetime for proper sorting
monthly_metrics['Month_dt'] = pd.to_datetime(
monthly_metrics['Month_completed'],
format='%b-%Y'
)
# Sort chronologically and format for display
monthly_metrics = monthly_metrics.sort_values('Month_dt')
monthly_metrics['Month'] = monthly_metrics['Month_dt'].dt.strftime('%b-%Y')
monthly_metrics = monthly_metrics.drop(columns=['Month_dt'])
# Data validation
if monthly_metrics.empty:
raise ValueError("No valid data available for plotting")
#--------- New 05/22 -------------
# Determine the dynamic span based on the DataFrame data
start_date = pd.to_datetime(df_AccessDB_filtered['Date completed'].min()).strftime('%m/%d/%Y')
end_date = pd.to_datetime(df_AccessDB_filtered['Date completed'].max()).strftime('%m/%d/%Y')
title_span = f"{start_date} to {end_date}"
#---------------------------------
source = ColumnDataSource(monthly_metrics)
# Create figure
p = figure(
x_range=monthly_metrics['Month'].tolist(),
#width=2400, # 05/16
width=1200, # 05/16
height=700,
tools=["hover", "pan", "box_zoom", "wheel_zoom", "reset", "save"],
toolbar_location="right",
#title="Monthly ECO Performance - ECO released from Engineering vs. 30 days target",
title=f"Monthly ECO Performance - ECO released vs. 30 days target - ECO released from: {title_span}", # 05/22
output_backend='svg'
)
# Formatting
p.title.text_font_size = "14pt"
p.xaxis.major_label_orientation = 45
p.xaxis.major_label_text_font_size = "10pt"
p.ygrid.grid_line_alpha = 0.3
p.xgrid.grid_line_color = None
# X-axis font size 04/02
p.xaxis.major_label_text_font_size = "12pt"
p.xaxis.major_label_text_font_style = "bold"
# Title text font
p.title.text_font_size = "14pt"
# Bars for completion time
bars = p.vbar(
x='Month',
top='Avg_Completion_Time',
width=0.6,
color='#00B050',
alpha=0.7,
legend_label="Avg Completion Time (days)",
source=source,
)
# Primary Y-axis
p.y_range = Range1d(start=0, end=monthly_metrics['Avg_Completion_Time'].max() * 1.2)
p.yaxis.axis_label = "Average Completion Time (days)"
p.yaxis.axis_label_text_color = "#00B050"
# Secondary Y-axis
max_jobs = monthly_metrics['Total_Jobs'].max()
p.extra_y_ranges = {"changes": Range1d(start=0, end=max(10, max_jobs * 1.2))}
secondary_axis = LinearAxis(
y_range_name="changes",
axis_label="Quantity of ECO released",
axis_label_text_color="#4472C4"
)
p.add_layout(secondary_axis, 'right')
# Target line
target_line = Span(
location=30,
dimension='width',
line_color='#7030A0',
line_dash='dashed',
line_width=2
)
p.add_layout(target_line)
# Add a purple "30 days" label on the left y-axis
thirty_days_label = Label(
x=20, # pixels from the left edge
y=32,
x_units='screen', # Position based on screen pixels
y_units='data',
text="30 days",
text_color="#7030A0",
text_font_size="12px",
text_align="left",
angle=90, # Rotate text by 90 degrees
angle_units="deg" # Specify degrees
)
p.add_layout(thirty_days_label)
# Line for number of changes
change_line = p.line(
x='Month',
y='Total_Jobs',
y_range_name="changes",
line_width=2,
color='#4472C4',
legend_label="Quantity of ECO released",
source=source
)
# Points for number of changes
change_points = p.scatter(
x='Month',
y='Total_Jobs',
y_range_name="changes",
marker="circle",
size=10,
fill_color='white',
line_color='#548235',
source=source
)
# Legend
p.legend.location = "top_right"
p.legend.click_policy = "hide"
# Add dahsed line for legend entry
p.add_layout(target_line)
# Create a Line glyph to represent the dashed line in the legend
line_renderer = p.line([0, 1], line_color="#7030A0", line_dash="dashed", line_width=2)
# Create a legend item for the dashed line (90% target line)
legend_item = LegendItem(label="30-Day Target", renderers=[line_renderer])
# Add this legend item to the plot's legend
p.legend.items.append(legend_item)
# Hover tool
hover = p.select_one(HoverTool)
hover.tooltips = [
("Month", "@Month"),
("Avg Completion", "@Avg_Completion_Time{0.0} days"),
("Quantity of ECO released", "@Total_Jobs")
]
hover.formatters = {
'@Avg_Completion_Time': 'numeral',
'@Total_Jobs': 'numeral'
}
p.toolbar.logo = None
return p
#------------------------------------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------------------------------------
# 05/16 - create_FTB_new_per_customer(df_AccessDB_filtered, selected_program)
#------------------------------------------------------------------------------------------------------------------
# Create a graph reprenting the number of ECO released per Customer using the exact same formating than create_customers_figure
# Datafram input is df_AccessDB_filtered 'Change#' represent every unique ECO released, 'Date completed' represent the completion date. 'Costumer' represent the customer
def create_FTB_new_per_customer(df_AccessDB_filtered, selected_program, logo_mapping):
"""
Creates a bar plot representing the number of ECOs released per customer with logos.
Parameters:
- df_AccessDB_filtered: DataFrame containing ECO data with 'Change#', 'Date completed', 'Costumer', and 'Program' columns
- selected_program: String representing the selected program name
- logo_mapping: Dictionary mapping customer names to logo URLs
Returns:
- p_eco: Figure showing ECO counts by customer
"""
# 1. Filter data for the selected program
df_filtered = df_AccessDB_filtered[df_AccessDB_filtered['Program'] == selected_program]
# 2. Aggregate ECO counts by customer
df_eco_summary = (
df_filtered.groupby('End Costumer')
.agg({'Change#': 'nunique'})
.reset_index()
.rename(columns={'Change#': 'ECO_Count', 'End Costumer': 'Customer'})
)
# 3. Calculate max value for plot scaling
max_eco_count = df_eco_summary['ECO_Count'].max()
# 4. Add logo URLs and offset
df_eco_summary['Logo'] = df_eco_summary['Customer'].map(logo_mapping)
logo_offset_eco = max(max_eco_count * 0.02, 2) # Reduced offset for closer logo placement
df_eco_summary['Logo_Offset_ECO'] = df_eco_summary['ECO_Count'] + logo_offset_eco
# 5. Create color palette
customers = df_eco_summary['Customer'].unique()
num_customers = len(customers)
base_palette = Category20[max(3, min(20, num_customers))]
pastel_colors = []
for color in base_palette[:num_customers]:
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
r = int(r + (255 - r) * 0.6)
g = int(g + (255 - g) * 0.6)
b = int(b + (255 - b) * 0.6)
pastel_colors.append(f'#{r:02x}{g:02x}{b:02x}')
customer_color_map = dict(zip(customers, pastel_colors))
df_eco_summary['Color'] = df_eco_summary['Customer'].map(customer_color_map)
# 6. Create data source for plot
source = ColumnDataSource(df_eco_summary)
# 7. Convert 'Date completed' to datetime and extract date range
df_filtered['Date completed'] = pd.to_datetime(df_filtered['Date completed'])
earliest_date = df_filtered['Date completed'].min().strftime('%b %Y')
latest_date = df_filtered['Date completed'].max().strftime('%b %Y')
# 8. Create ECO count plot
p_eco = figure(
x_range=df_eco_summary['Customer'].tolist(),
title=f"Total ECOs Released per Customer, Program: {selected_program} - [{earliest_date} to {latest_date}]",
x_axis_label='Customer',
y_axis_label='ECO Count',
tools="pan,wheel_zoom,save,reset",
y_range=(0, max_eco_count * 1.5),
height=700,
width=1200,
min_height=400,
output_backend='svg'
)
p_eco.vbar(
x='Customer',
top='ECO_Count',
width=0.4,
source=source,
fill_color='Color',
line_color='Color',
alpha=0.6
)
p_eco.add_tools(HoverTool(
tooltips=[('Customer', '@Customer'), ('ECO Count', '@ECO_Count{0,0}')]
))
# 9. Add logos to the plot
p_eco.add_glyph(
source,
ImageURL(
url='Logo',
x='Customer',
y='Logo_Offset_ECO',
anchor="center",
)
)
# 10. Apply common formatting
p_eco.title.text_font_size = "14pt"
p_eco.xaxis.major_label_text_font_size = '12pt'
p_eco.xaxis.major_label_text_font_style = "bold"
p_eco.xaxis.major_label_orientation = 1.2
p_eco.ygrid.grid_line_dash = [6, 4]
p_eco.xgrid.visible = False
p_eco.toolbar.logo = None
p_eco.yaxis.formatter = NumeralTickFormatter(format="0,0")
return p_eco
#------------------------------------------------------------------------------------------------------------------
# 04/04WIP - Create new Datafram 'create_wip_dataframe_dashboard' from df_Backlog and df_WIP
#------------------------------------------------------------------------------------------------------------------
# Starts with df_WIP and merge info from df_Backlog (sale price, potential NRE and FAI fess)
df_production_wip = df_WIP.copy()
# 04/18 - Keep data only for 'Site' = 100 (Redmond)
df_production_wip = df_production_wip[df_production_wip['Site'] == 100]
#print('df_Backlog:')
#display(df_Backlog.head())
# filtered_df_wip = filtered_df_wip[~filtered_df_wip['WO'].str.contains('NC', na=False)]
#------------------------------------------------------------------------------------------------------------------
# Updated 04/18
def create_wip_dataframe_dashboard(df_production_wip_filtered, df_Backlog_filtered, selected_program, df_Historic_dashboard_filtered):
filtered_df_wip = df_production_wip_filtered.copy()
df_Backlog_wip = df_Backlog_filtered.copy()
# Create date of report
date_report = pd.to_datetime(file_date)
formatted_date = date_report.strftime('%m-%d-%Y')
# Filter data
filtered_df_wip = filtered_df_wip[filtered_df_wip['Level'] == 0] # Filter on 'Level'
df_Backlog_wip = df_Backlog_wip[~df_Backlog_wip['Order'].str.contains('NC', na=False)]
filtered_df_wip = filtered_df_wip[~filtered_df_wip['WO'].str.contains('NC', na=False)]
backlog_first = df_Backlog_wip.groupby('IDD Top Level').first().reset_index()
backlog_first['Sales Price per Unit ($)'] = backlog_first.apply(
lambda row: row['Currency net amount'] / row['Backlog Qty'] if row['Backlog Qty'] != 0 else 0,
axis=1
)
price_map = backlog_first.set_index('IDD Top Level')[['Sales Price per Unit ($)', 'NRE Fee', 'FAI Fee']].to_dict()
filtered_df_wip['Sales Price per Unit ($)'] = filtered_df_wip['IDD Component'].map(price_map['Sales Price per Unit ($)']).fillna(0)
filtered_df_wip['NRE Fee'] = filtered_df_wip['IDD Component'].map(price_map['NRE Fee']).fillna(0)
filtered_df_wip['FAI Fee'] = filtered_df_wip['IDD Component'].map(price_map['FAI Fee']).fillna(0)
filtered_df_wip['Sales value current WO ($)'] = filtered_df_wip['Sales Price per Unit ($)'] * filtered_df_wip['WO Qty']
# Handle empty DataFrame
if filtered_df_wip.empty:
processed_df = pd.DataFrame({
'Pty Indice': ['No Data'], 'WO': [''], 'WO Qty': [''], 'Last movement': [''], 'Work Ctr: ['']'
'Area': [''], 'IDD Component': [''], 'Customer PN': [''],
'Description Component': [''], 'Release': [''], 'Sales Price per Unit ($)': [''],
'Sales value current WO ($)': [''], 'NRE Fee': [''], 'FAI Fee': ['']
})
else:
# Parse dates
filtered_df_wip['Last movement'] = pd.to_datetime(filtered_df_wip['Last movement'], errors='coerce')
filtered_df_wip['Release'] = pd.to_datetime(filtered_df_wip['Release'], errors='coerce')
# Group by WO and get the most recent row
def select_most_recent(group):
return group.loc[group['Last movement'].idxmax()] if group['Last movement'].notna().any() else group.iloc[0]
filtered_df_wip = filtered_df_wip.groupby('WO', group_keys=False).apply(select_most_recent)
filtered_df_wip = filtered_df_wip.sort_values(by='Release')
# Format dates
filtered_df_wip['Last movement'] = filtered_df_wip['Last movement'].apply(
lambda x: x.strftime('%m-%d-%Y') if pd.notna(x) else ''
)
filtered_df_wip['Release'] = filtered_df_wip['Release'].apply(
lambda x: x.strftime('%m-%d-%Y') if pd.notna(x) else ''
)
# Handle missing column
if 'SEDA Top Level' in filtered_df_wip.columns:
filtered_df_wip = filtered_df_wip.rename(columns={'SEDA Top Level': 'Customer PN'})
else:
filtered_df_wip['Customer PN'] = filtered_df_wip['IDD Component']
filtered_df_wip = filtered_df_wip[[
'Pty Indice', 'WO', 'WO Qty', 'Last movement', 'Work Ctr', 'Area', 'IDD Component',
'Customer PN', 'Description Component', 'Release',
'Sales Price per Unit ($)', 'Sales value current WO ($)', 'NRE Fee', 'FAI Fee'
]]
filtered_df_wip['Customer PN'] = filtered_df_wip['Customer PN'].fillna('N/A')
filtered_df_wip = filtered_df_wip.sort_values(by=['Pty Indice'])
cols_to_round = ['Sales Price per Unit ($)', 'Sales value current WO ($)', 'NRE Fee', 'FAI Fee']
for col in cols_to_round:
filtered_df_wip[col] = pd.to_numeric(filtered_df_wip[col], errors='coerce').fillna(0)
filtered_df_wip[cols_to_round] = filtered_df_wip[cols_to_round].round(1)
processed_df = filtered_df_wip
# New Calculations for Summary Metrics
''' SAVED 04/17
# Total sale value on the floor (excluding 'Area' = 'Kitting' - Not case sensitive)
total_sales_excl_kitting = processed_df[processed_df['Area'].str.lower() != 'kitting']['Sales value current WO ($)'].sum()
# Total sale value released (kitting included) - already calculated
total_sales = processed_df['Sales value current WO ($)'].sum()
# Number of WO on the floor (excluding 'Area' = 'Kitting' - Not case sensitive)
unique_wos_excl_kitting = processed_df[processed_df['Area'].str.lower() != 'kitting']['WO'].nunique()
'''
#---- 04/17 -------------------------------------------
# New Calculations for Summary Metrics
# Ensure 'Area' is string and handle missing values
processed_df['Area'] = processed_df['Area'].fillna('').astype(str)
# Total sale value on the floor (excluding 'Area' = 'Kitting' - Not case sensitive)
total_sales_excl_kitting = processed_df[processed_df['Area'].str.lower() != 'kitting']['Sales value current WO ($)'].sum()
# Total sale value released (kitting included) - already calculated
total_sales = processed_df['Sales value current WO ($)'].sum()
# Number of WO on the floor (excluding 'Area' = 'Kitting' - Not case sensitive)
unique_wos_excl_kitting = processed_df[processed_df['Area'].str.lower() != 'kitting']['WO'].nunique()
#-------------------------------------------------------
# Number of WO released (kitting included)
unique_wos = processed_df['WO'].nunique()
# Total fees value
total_fees = processed_df['NRE Fee'].sum() + processed_df['FAI Fee'].sum()
# Number of unique assemblies
unique_assemblies = processed_df['Pty Indice'].nunique()
# Number of First Time Build/Prototype (WO containing 'F')
first_time_builds = processed_df[processed_df['WO'].str.contains('F', na=False)]['WO'].nunique()
# Shipment past couple weeks (2 weeks from file_date)
if df_Historic_dashboard_filtered is not None:
two_weeks_ago = date_report - pd.Timedelta(weeks=2)
recent_shipments = df_Historic_dashboard_filtered[
(pd.to_datetime(df_Historic_dashboard_filtered['Invoice date']) >= two_weeks_ago) &
(pd.to_datetime(df_Historic_dashboard_filtered['Invoice date']) <= date_report)
]['Qty Shipped'].sum()
else:
recent_shipments = 0 # Default if df_Historic_dashboard_filtered is not provided
# Updated Summary HTML
summary_html = f"""
<div style="text-align:left; font-size:16px; line-height:2; padding:30px;">
<b>Total sale value on the floor:</b> ${total_sales_excl_kitting:,.2f}<br>
<b>Total sale value released</b> (kitting included): ${total_sales:,.2f}<br>
<b>Number of WO on the floor:</b> {unique_wos_excl_kitting} WO<br>
<b>Number of WO released </b> (kitting included): {unique_wos} WO<br>
<b>Total fees value on the floor:</b> ${total_fees:,.2f}<br>
<b>Number of unique assemblies:</b> {unique_assemblies}<br>
<b>Number of First Time Build WO/Prototype:</b> {first_time_builds} WO<br>
<hr style="border: 1px solid lightgray;">
<b>Shipment the past couple weeks:</b> {recent_shipments:,} units
</div>
"""
# Style the DataFrame for Panel display (unchanged)
styled_df = processed_df.style.set_properties(**{
'text-align': 'center',
'vertical-align': 'middle'
}).set_table_styles([
{'selector': 'th', 'props': [('text-align', 'center'), ('vertical-align', 'middle')]}
]).format({
'Sales Price per Unit ($)': '{:.1f}',
'Sales value current WO ($)': '{:.1f}',
'NRE Fee': '{:.1f}',
'FAI Fee': '{:.1f}'
}).hide(axis='index')
# Save Button with Excel Formatting (unchanged)
def save_to_excel(event):
save_button.loading = True
try:
if not selected_program:
raise ValueError("No program selected.")
filename = f"{selected_program}_Top-Level_WIP-Redmond_{formatted_date}.xlsx"
save_path = os.path.join(os.getcwd(), filename)
wb = Workbook()
ws = wb.active
ws.title = "WIP Dashboard"
for r in dataframe_to_rows(processed_df, index=False, header=True):
ws.append(r)
font_color_black = '000000'
alignment = Alignment(horizontal="center", vertical="center")
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(color='FFFFFF', bold=True)
thin_border = Border(left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side("thin"))
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = alignment
cell.border = thin_border
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=1, max_col=ws.max_column):
for cell in row:
cell.font = Font(color=font_color_black)
cell.alignment = alignment
cell.border = thin_border
for col in ws.columns:
column_letter = get_column_letter(col[0].column)
ws.column_dimensions[column_letter].width = 15
for col_letter in ['D', 'E', 'F', 'I']:
ws.column_dimensions[col_letter].width = 20
ws.column_dimensions['H'].width = 25
ws.auto_filter.ref = ws.dimensions
wb.save(save_path)
print(f"Table saved to {save_path}")
if hasattr(pn.state, 'notifications') and pn.state.notifications:
pn.state.notifications.success(f"Table saved: {filename}")
except Exception as e:
error_msg = f"❌ Save failed: {str(e)}"
print(error_msg)
if hasattr(pn.state, 'notifications') and pn.state.notifications:
pn.state.notifications.error(error_msg)
finally:
save_button.loading = False
save_button = pn.widgets.Button(name="Save to Excel", button_type="primary", width=200)
save_button.on_click(save_to_excel)
# Create Panel panes (unchanged)
table_pane = pn.pane.DataFrame(styled_df, width=1900, height=700)
summary_pane = pn.pane.HTML(summary_html, width=500, height=650)
Production_Dashboard_Dataframe = pn.Row(
table_pane,
pn.Column(
summary_pane,
save_button,
align="start"
),
align="center"
)
return Production_Dashboard_Dataframe
#------------------------------------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------------------------------------
# 04/07 - Heatmap
#------------------------------------------------------------------------------------------------------------------
# uodate 04/11 to exclude 'Manager'
def create_monthly_engineering_hours_heatmap(df_SciformaReport_Team_filtered, selected_program):
# New 04/11 - Filter out rows where 'Function' is 'Manager'
df_SciformaReport_Team_filtered = df_SciformaReport_Team_filtered[df_SciformaReport_Team_filtered['Function'] != 'Manager']
# Data Preparation
df = df_SciformaReport_Team_filtered.copy()
df = df[df['Program'] == selected_program]
# Dynamically identify month columns in the format YYYY-MM using regex
month_columns = [col for col in df.columns if re.match(r'^\d{4}-\d{2}$', col)]
if not month_columns:
month_columns = []
# Sort month columns chronologically
month_columns = sorted(month_columns, key=lambda x: x) # Sorts by string, which works for YYYY-MM format
if not all(col in df.columns for col in ['Program', 'Function']) or not month_columns:
p = figure(
title=f"Percentage of Engineering Hours by Team - Program: {selected_program}",
width=1200,
height=700,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='right',
output_backend='svg'
)
p.text(
x=[0], y=[50], text=["Invalid or missing data in DataFrame"],
text_align="center", text_font_size="12pt"
)
p.y_range = Range1d(0, 100)
p.xgrid.visible = False
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.toolbar.logo = None
return p
# Reshape the DataFrame
df = df.melt(
id_vars=['Program', 'Function'],
value_vars=month_columns,
var_name='Month',
value_name='Hours'
)
df['Month'] = pd.to_datetime(df['Month'], format='%Y-%m')
df['Month_Year'] = df['Month'].dt.strftime('%b %Y')
monthly_data = df.groupby(['Function', 'Month_Year'])['Hours'].sum().reset_index()
monthly_totals = monthly_data.groupby('Month_Year')['Hours'].sum().reset_index()
monthly_totals.rename(columns={'Hours': 'Total_Hours'}, inplace=True)
monthly_data = monthly_data.merge(monthly_totals, on='Month_Year')
monthly_data['Percentage'] = (monthly_data['Hours'] / monthly_data['Total_Hours']) * 100
monthly_data['Percentage'] = monthly_data['Percentage'].fillna(0)
if monthly_data.empty:
p = figure(
title=f"Percentage of Engineering Hours by Team - Program: {selected_program}",
width=1200,
height=700,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='right',
output_backend='svg'
)
p.text(
x=[0], y=[50], text=["No engineering hours data available for the selected program"],
text_align="center", text_font_size="12pt"
)
p.y_range = Range1d(0, 100)
p.xgrid.visible = False
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
p.toolbar.logo = None
return p
source = ColumnDataSource(monthly_data)
unique_months = sorted(monthly_data['Month_Year'].unique(),
key=lambda x: pd.to_datetime(x, format='%b %Y'))
unique_functions = sorted(monthly_data['Function'].unique())
p = figure(
x_range=unique_months,
y_range=unique_functions,
title=f"Percentage of Engineering Hours Consumed by Team - Program: {selected_program}",
width=1200,
height=700,
tools='pan,wheel_zoom,box_zoom,reset,save,hover',
toolbar_location='right',
x_axis_label='Month',
y_axis_label='Function',
output_backend='svg'
)
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = "#E0E0E0"
p.ygrid.grid_line_dash = [6, 4]
# X-axis font size
p.xaxis.major_label_text_font_size = "12pt"
p.xaxis.major_label_text_font_style = "bold"
# Title text font
p.title.text_font_size = "14pt"
# Custom pastel Viridis-like gradient (purple → blue → green → yellow)
pastel_viridis = [
"#E6E4EA", # Light pastel purple (0%)
"#D7D9E5", # Slightly lighter purple
"#C8CEE0", # Soft purple-blue
"#B9C3DB", # Muted blue
"#AAC8D6", # Pastel teal
"#BBDED6", # Light teal-green
"#CCE4D1", # Pastel green
"#DDE9CC", # Light green
"#EEEFC7", # Pale yellow-green
"#FFF5C2" # Soft pastel yellow (100%)
]
color_mapper = LinearColorMapper(palette=pastel_viridis, low=0, high=100)
p.rect(
x='Month_Year',
y='Function',
width=1,
height=1,
source=source,
fill_color=transform('Percentage', color_mapper),
line_color=None
)
color_bar = ColorBar(
color_mapper=color_mapper,
ticker=BasicTicker(desired_num_ticks=10),
label_standoff=12,
border_line_color=None,
location=(0, 0),
title="Percentage (%)"
)
p.add_layout(color_bar, 'right')
hover = HoverTool(
tooltips=[
("Month", "@Month_Year"),
("Function", "@Function"),
("Hours", "@Hours{0,0}"),
("Percentage", "@Percentage{0.1f}%")
]
)
p.hover.tooltips = hover.tooltips
p.xaxis.major_label_orientation = 1.2
p.min_border_left = 100
p.min_border_right = 100
p.toolbar.logo = None
return p
#------------------------------------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------------------------------------
# 04/10 - Quality - Dataframe NC Received per Assembly
#------------------------------------------------------------------------------------------------------------------
df_Historic_complete = pd.read_excel(input_file_formatted, sheet_name='CM-Historic', index_col=False)
# Change 'Phase 4' or 'Phase 5' with 'Phase 4-5'
if 'Program' in df_Historic_complete.columns and 'Pty Indice' in df_Historic_complete.columns:
mask = (
df_Historic_complete['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Historic_complete['Pty Indice'].str.contains('Phase5', na=False)
)
df_Historic_complete.loc[mask, 'Program'] = 'Phase 4-5'
#------------------------------------------------------------------------------------------------------------------
# 04/11 - Apply determine_category to create the column 'Product Category' in df_Historic_complete based on 'Description'
df_Historic_complete['Product Category'] = df_Historic_complete['Pty Indice'].apply(determine_category)
#------------------------------------------------------------------------------------------------------------------
def create_NC_received_per_assembly(df_Historic_complete_filtered, selected_program):
# Verify input dataframe
if df_Historic_complete_filtered is None or df_Historic_complete_filtered.empty:
print("Debug: df_Historic_complete_filtered is None or empty")
p = pn.Column(
pn.pane.HTML(f"""
<h3 style='text-align: center; margin-bottom: 15px;'>
Program: {selected_program} - NC Received per Assembly
</h3>
<p style='text-align: center; color: red;'>
No data available for NC Received per Assembly
</p>
"""),
sizing_mode='stretch_width'
)
return p
# Create a copy of the dataframe
df = df_Historic_complete_filtered.copy()
# Check required columns
required_cols = ['Order', 'Quantity', 'Pty Indice', 'IDD Top Level', 'SEDA Top Level']
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
raise KeyError(f"Missing required columns: {missing_cols}")
# Split data into NC and non-NC orders
df_nc = df[df['Order'].str.contains('NC', na=False)].copy()
df_non_nc = df[~df['Order'].str.contains('NC', na=False)].copy()
#print(f"Debug: NC orders count (before filtering negative quantities): {len(df_nc)}, Non-NC orders count: {len(df_non_nc)}")
# Filter out NC orders with negative quantities
df_nc = df_nc[df_nc['Quantity'] > 0]
#print(f"Debug: NC orders count (after filtering negative quantities): {len(df_nc)}")
# Calculate Qty NC received (only for positive quantities)
nc_received = df_nc.groupby(['Pty Indice', 'IDD Top Level', 'SEDA Top Level'])['Quantity'].sum().reset_index()
nc_received = nc_received.rename(columns={'Quantity': 'Qty NC received'})
# Calculate total Qty shipped from non-NC orders
qty_shipped = df_non_nc.groupby(['Pty Indice', 'IDD Top Level', 'SEDA Top Level'])['Quantity'].sum().reset_index()
qty_shipped = qty_shipped.rename(columns={'Quantity': 'Qty shipped'})
# Merge and calculate percentage
result_df = pd.merge(nc_received, qty_shipped, on=['Pty Indice', 'IDD Top Level', 'SEDA Top Level'], how='outer').fillna(0)
result_df['% NC received vs. Qty shipped'] = (result_df['Qty NC received'] / result_df['Qty shipped'] * 100).fillna(0).round(2)
result_df.loc[result_df['Qty shipped'] == 0, '% NC received vs. Qty shipped'] = 0
# Filter for rows where Qty NC received > 0
result_df = result_df[result_df['Qty NC received'] > 0]
# If no rows have NC > 0, return a "no data" message
if result_df.empty:
print("Debug: No assemblies with NC received > 0 after filtering")
p = pn.Column(
pn.pane.HTML(f"""
<h3 style='text-align: center; margin-bottom: 15px;'>
Program: {selected_program} - NC Received per Assembly
</h3>
<p style='text-align: center; color: red;'>
No assemblies with NC received for this program
</p>
"""),
sizing_mode='stretch_width'
)
return p
# Sort by Pty Indice
result_df = result_df.sort_values('Pty Indice')
# Rename 'Pty Indice' to 'pty'
result_df = result_df.rename(columns={'Pty Indice': 'pty'})
# Style the dataframe
styled_df = result_df.style.set_properties(**{
'text-align': 'center',
'vertical-align': 'middle'
}).set_table_styles([
{'selector': 'th', 'props': [('text-align', 'center'), ('vertical-align', 'middle')]}
]).format({
'Qty NC received': '{:,.0f}',
'Qty shipped': '{:,.0f}',
'% NC received vs. Qty shipped': '{:.2f}%'
}).hide(axis='index')
# Create the Panel layout
p = pn.Column(
pn.pane.HTML(f"""
<h3 style='text-align: center; margin-bottom: 15px;'>
Program: {selected_program} - NC Received per Assembly
</h3>
"""),
pn.pane.DataFrame(styled_df, width=1200, height=550),
sizing_mode='stretch_width'
)
return p
#------------------------------------------------------------------------------------------------------------------
##################################################################
# Card 'Monthly metrics - Previous month vs previous previous month
##################################################################
def calculate_monthly_metrics(df_Historic_dashboard_filtered):
current_date = pd.to_datetime('today')
current_month = current_date.month
current_year = current_date.year
# Calculate the month and year for the previous month
if current_month > 1:
previous_month = current_month - 1
previous_month_year = current_year
else:
previous_month = 12
previous_month_year = current_year - 1
# Calculate the month and year for the month before the previous month
if previous_month > 1:
two_months_ago = previous_month - 1
two_months_ago_year = previous_month_year
else:
two_months_ago = 12
two_months_ago_year = previous_month_year - 1
# Filter and aggregate data for the previous month
df_Previous_Month = df_Historic_dashboard_filtered[
(df_Historic_dashboard_filtered['Month'] == previous_month) &
(df_Historic_dashboard_filtered['Year'] == previous_month_year)
].agg({
'Qty Shipped': 'sum',
'Sales USD': 'sum'
}).to_dict()
# Filter and aggregate data for the month before the previous month
df_Two_Months_Ago = df_Historic_dashboard_filtered[
(df_Historic_dashboard_filtered['Month'] == two_months_ago) &
(df_Historic_dashboard_filtered['Year'] == two_months_ago_year)
].agg({
'Qty Shipped': 'sum',
'Sales USD': 'sum'
}).to_dict()
# Extract aggregated values
total_shipped_previous = int(df_Previous_Month['Qty Shipped'])
total_sales_previous = int(df_Previous_Month['Sales USD'])
total_shipped_two_months_ago = int(df_Two_Months_Ago['Qty Shipped'])
total_sales_two_months_ago = int(df_Two_Months_Ago['Sales USD'])
# Calculate percentage changes
if total_shipped_two_months_ago != 0:
pct_change_shipped = ((total_shipped_previous - total_shipped_two_months_ago) / total_shipped_two_months_ago) * 100
else:
pct_change_shipped = float('inf') if total_shipped_previous > 0 else float('-inf')
if total_sales_two_months_ago != 0:
pct_change_sales = ((total_sales_previous - total_sales_two_months_ago) / total_sales_two_months_ago) * 100
else:
pct_change_sales = float('inf') if total_sales_previous > 0 else float('-inf')
return {
'total_shipped_previous': total_shipped_previous,
'total_sales_previous': total_sales_previous,
'total_shipped_two_months_ago': total_shipped_two_months_ago,
'total_sales_two_months_ago': total_sales_two_months_ago,
'pct_change_shipped': pct_change_shipped,
'pct_change_sales': pct_change_sales
}
# update 03/20
def create_monthly_metrics_indicator(df_Historic_dashboard_filtered):
# Calculate the current date and determine the previous month and year
today = pd.to_datetime('today')
if today.month > 1:
previous_month = today.month - 1
previous_month_year = today.year
else:
previous_month = 12
previous_month_year = today.year - 1
# Calculate the month and year for two months ago
if previous_month > 1:
two_months_ago = previous_month - 1
two_months_ago_year = previous_month_year
else:
two_months_ago = 12
two_months_ago_year = previous_month_year - 1
# Convert month numbers to month names
month_names = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
previous_month_name = month_names[previous_month - 1]
two_months_ago_name = month_names[two_months_ago - 1]
# Calculate metrics for the previous month compared to two months ago
metrics = calculate_monthly_metrics(df_Historic_dashboard_filtered)
# Determine trend colors and arrows
trend_color_shipped = "green" if metrics['pct_change_shipped'] >= 0 else "red"
trend_arrow_shipped = "▲" if metrics['pct_change_shipped'] >= 0 else "▼"
trend_color_sales = "green" if metrics['pct_change_sales'] >= 0 else "red"
trend_arrow_sales = "▲" if metrics['pct_change_sales'] >= 0 else "▼"
# Create HTML content with trend information
html_content = f"""
<div style="font-size: 20px; font-family: Arial, sans-serif; padding: 10px; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;">
<h3 style="margin: 0; color: teal;">Monthly comparison - {previous_month_name} vs. {two_months_ago_name} {two_months_ago_year}</h3>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total Quantity Shipped {previous_month_name}</strong> (vs. {two_months_ago_name}): <span style="color: black;">{metrics['total_shipped_previous']:,.0f}</span>
<span style="font-size: 14px; color: {trend_color_shipped};">({trend_arrow_shipped} {metrics['pct_change_shipped']:+,.1f}%)</span>
</p>
<p style="margin: 10px 0; font-size: 16px;">
<strong>Total realized Sales {previous_month_name}</strong> (vs. {two_months_ago_name}): <span style="color: black;">${metrics['total_sales_previous']:,.0f}</span>
<span style="font-size: 14px; color: {trend_color_sales};">({trend_arrow_sales} {metrics['pct_change_sales']:+,.1f}%)</span>
</p>
</div>
"""
# Create layout with indicators and HTML content
layout = pn.Column(
pn.pane.HTML(html_content, sizing_mode='stretch_width'),
sizing_mode='stretch_width'
)
return layout
#/////////////////////////////////////////////////////////////
##############################################################
# Creation of Card and Layout
##############################################################
#/////////////////////////////////////////////////////////////
##############################################
# Defining color and dimensions for the cards
###############################################
# Define style properties for each card
# background_color = Header background color
# font_color = Header text font color
# width & height are the dimension of the card, and margin is the margin around the card (horizontal, vertical)
card_styles = {
"YoY Sales": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200, # 03/28
"height": 700,
"margin": (5, 5),
"font_weight": "bold"
},
"YoY Shipments": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200, # 03/28
"height": 700,
"margin": (5, 5),
"font_weight": "bold"
},
"Total Shipment per customer": {
"background_color": "#aee0d9",
"font_color": "white",
"width": 1200,
"height": 700,
"margin": (5, 5),
"font_weight": "bold"
},
"Total sales per customer": {
"background_color": "#aee0d9",
"font_color": "white",
"width": 1200,
"height": 700,
"margin": (5, 5),
"font_weight": "bold"
},
"Yearly KPI": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 650,
"height": 200,
"margin": (5, 5),
"font_weight": "bold"
},
"Cumulative KPI": {
"background_color": "#aee0d9",
"font_color": "white",
"width": 650,
"height": 200,
"margin": (5, 5),
"font_weight": "bold"
},
"Monthly KPI": {
"background_color": "#a8c1a5",
"font_color": "white",
"width": 650,
"height": 200,
"margin": (5, 5),
"font_weight": "bold"
},
# 02/27 - New card
"Yearly Sales and Shipment": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 650,
"height": 250,
"margin": (5, 5),
"font_weight": "bold"
},
# 03/04 - RMA card
"Yearly RMA": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 650,
"height": 250,
"margin": (5, 5),
"font_weight": "bold"
},
# 03/04 - RMA raph
"Monthly RMA": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700,
"margin": (5, 5),
"font_weight": "bold"
},
# 03/19 - Backlog card
"Yearly KPI - Backlog": {
"background_color": "#98c6e5", # Use the same color as other yearly KPIs
"font_color": "white",
"width": 650,
"height": 250,
"margin": (5, 5),
"font_weight": "bold"
},
# 03/25 - Graph Sales and Fees
"Monthly Sales and Fees": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700,
"margin": (5, 5),
"font_weight": "bold"
},
# 03/27 - Card most critical backlog
"Backlog - Most Critical Assemblies": {
"background_color": "#ff6f61", # Reddish for criticality
"font_color": "white",
"width": 415,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
# 03/28 - Graph create_monthly_nre_fees_graph
"Monthly NRE Fees": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
# 04/01 - Graph create_monthly_engineering_hours_graph
"Monthly Engineering Hours Consumed per Team": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 2400,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
# 04/02 - Graph create_monthly_engineering_cumulative_hours_graph
"Monthly Engineering Cumulative Hours Consumed": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
# 04/02 - Graph create_engineering_monthly_ECO_KPI_Graph # 05/16 update size
"Monthly ECO KPI": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
#04/04 - Table create_wip_dataframe_dashboard
"Production - Assemblies on the floor at IDD in Redmond": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 2400,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
#04/07 - Table create_monthly_engineering_hours_heatmap
"Engineering - Heatmap": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
#04/10 - Table create_NC_received_per_assembly
"Quality - Quality - RMA Received per Assembly": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
#05/16 - Graph New Product Introduction
"Engineering - New Product Introduction": {
"background_color": "#98c6e5",
"font_color": "white",
"width": 1200,
"height": 700, # Increased height to accommodate graph
"margin": (5, 5),
"font_weight": "bold"
},
}
###############################################
# Defining function
###############################################
def create_card_indicators(card_title, panel_object, styles):
# Ensure the panel object has responsive sizing
panel_object.sizing_mode = 'stretch_both'
# Create inline HTML for the card header and card styles (border, round corners)
card_style = f"""
<style>
.custom-card {{
border: 2px solid {styles["background_color"]}; /* Border matching header background */
border-radius: 10px; /* Rounded corners for the card */
overflow: hidden; /* Ensure content fits within the card */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for aesthetics */
}}
.custom-card-header {{
background-color: {styles["background_color"]}; /* Header background */
color: {styles["font_color"]}; /* Header font color */
padding: 10px;
font-size: 16px;
text-align: center;
font-weight: bold; /* Make text bold */
width: 100%;
border-top-left-radius: 10px; /* Rounded top corners */
border-top-right-radius: 10px; /* Rounded top corners */
}}
</style>
"""
header_html = f"<div class='custom-card-header'>{card_title}</div>"
# Create the card layout with the header and the panel object
card = pn.Column(
pn.pane.HTML(card_style + header_html), # Header with custom style
panel_object, # The panel object (e.g., plot or indicator)
width=styles["width"],
height=styles["height"],
sizing_mode='fixed',
margin=styles["margin"],
css_classes=['custom-card'] # Applying the custom card style
)
return card
#-----------------------------------------------------------------------------
# 02/27 - Define a function to calculate yearly metrics KPI
#-----------------------------------------------------------------------------
def calculate_yearly_sales_shipment(df_Historic_dashboard_filtered):
# Get the current year
current_year = pd.to_datetime('today').year
# Initialize a dictionary to store yearly metrics
KPI_yearly_metrics = {}
# Loop through the years (e.g., 2022, 2023, 2024)
for year in range(2022, current_year + 1):
# Filter data for the specific year
df_year = df_Historic_dashboard_filtered[df_Historic_dashboard_filtered['Year'] == year]
# Aggregate metrics
total_shipped = int(df_year['Qty Shipped'].sum())
total_sales = int(df_year['Sales USD'].sum())
# Store metrics in the dictionary
KPI_yearly_metrics[year] = {
'total_shipped': total_shipped,
'total_sales': total_sales
}
return KPI_yearly_metrics
def create_yearly_sales_shipment_card(df_Historic_dashboard_filtered):
# Calculate yearly metrics
KPI_yearly_metrics = calculate_yearly_sales_shipment(df_Historic_dashboard_filtered)
# Create HTML content for the card
html_content = """
<div style="font-size: 20px; font-family: Arial, sans-serif; padding: 10px; border: 1px solid #ddd; border-radius: 5px; width: 100%; box-sizing: border-box;">
<h3 style="margin: 0; color: teal;">Yearly Sales and Shipment</h3>
"""
# Add metrics for each year with color coding, excluding zero values
for year, metrics in KPI_yearly_metrics.items():
if metrics['total_shipped'] == 0 and metrics['total_sales'] == 0:
continue # Skip years with zero sales and shipment
# Get the color for the year from the year_color_map
year_color = year_color_map.get(year, 'black') # Default to black if year not in map
html_content += f"""
<p style="margin: 10px 0; font-size: 16px;">
<span style="color: {year_color}; font-weight: bold;">Year {year}:</span>
<strong>{metrics['total_shipped']:,.0f}</strong> PN shipped, <strong>${metrics['total_sales'] / 1000:,.1f}K</strong> of sales
</p>
"""
# Close the HTML content
html_content += "</div>"
# Create a layout with the HTML content
yearly_sales_shipment_layout = pn.Column(
pn.pane.HTML(html_content, sizing_mode='stretch_width'),
sizing_mode='stretch_width'
)
return yearly_sales_shipment_layout # Return the unique layout variable
#-----------------------------------------------------------------------------
#//////////////////////////////////////////////////////////////
################################################################
# Data update & Update cards
################################################################
#//////////////////////////////////////////////////////////////
# Function to update the cards when the data changes
''' SAVED 04/10
def update_cards(event):
# Update data and re-render the layout
new_layout = update_data_dashboard(event, file_date) # Get the updated layout from update_data_dashboard # 03/28 update_data_dashboard
cover_dashboard[-1] = new_layout # Replace the last item in the column with the updated layout
'''
def update_cards(event):
global file_date
# Ensure file_date is set
if 'file_date' not in globals() or file_date is None:
file_date = pd.to_datetime(datetime.now().strftime("%m-%d-%Y"))
print("Warning: file_date was not set in update_cards, defaulting to today:", file_date)
print('file_date:', file_date)
# Update data and re-render the layout
new_layout = update_data_dashboard(event, file_date)
cover_dashboard[-1] = new_layout
# Attach the update_cards function to the program selection widget
program_widget_List.param.watch(update_cards, 'value') # Added 10/21
# Attach update_cards to toggle changes
toggle_lightplate.param.watch(update_cards, 'value') # Watch for changes in toggle_lightplate
toggle_others.param.watch(update_cards, 'value') # Watch for changes in toggle_others
# Data update function - # updated 03/28
def update_data_dashboard(event, file_date):
global span_report_historic_dashboard # Access the global variable
selected_program = program_widget_List.value
#-------- Debugg 04/11 -----------------------
print(f"update_data_dashboard received file_date: {file_date} (type: {type(file_date)})")
#--------------------------------------------
# Ensure file_date is a datetime object --> file_date_datetime
file_date_datetime = pd.to_datetime(file_date)
# Define the 1-year span for '1stTB'
start_date = file_date_datetime - pd.Timedelta(days=365) if selected_program == '1stTB' else None
end_date = file_date_datetime if selected_program == '1stTB' else None
# Create copies of all dataframes to avoid modifying globals
df_Historic_dashboard_filtered = df_Historic_dashboard.copy()
df_Priority_dashboard_filtered = df_Priority_dashboard.copy()
df_Historic_NC_filtered = df_Historic_NC.copy()
df_Backlog_filtered = df_Backlog_KPI.copy()
df_Snapshot_KPI_filtered = df_Snapshot_KPI.copy()
df_SciformaReport_filtered = df_SciformaReport.copy()
df_SciformaReport_Team_filtered = df_SciformaReport_Team.copy()
df_production_wip_filtered = df_production_wip.copy()
df_Historic_complete_filtered = df_Historic_complete.copy()
df_AccessDB_filtered = df_AccessDB.copy()
# Apply 1-year filter for '1stTB' to specified dataframes
if selected_program == '1stTB':
# Filter df_Historic_dashboard_filtered (uses 'Invoice date')
df_Historic_dashboard_filtered = df_Historic_dashboard_filtered[
(df_Historic_dashboard_filtered['Invoice date'] >= start_date) &
(df_Historic_dashboard_filtered['Invoice date'] <= end_date)
]
# Filter df_Priority_dashboard_filtered (assumes 'Invoice date')
if 'Invoice date' in df_Priority_dashboard_filtered.columns:
df_Priority_dashboard_filtered = df_Priority_dashboard_filtered[
(df_Priority_dashboard_filtered['Invoice date'] >= start_date) &
(df_Priority_dashboard_filtered['Invoice date'] <= end_date)
]
else:
print("Warning: 'Invoice date' not found in df_Priority_dashboard_filtered. 1-year filter not applied.")
# Filter df_Historic_NC_filtered (uses 'Invoice date')
df_Historic_NC_filtered = df_Historic_NC_filtered[
(df_Historic_NC_filtered['Invoice date'] >= start_date) &
(df_Historic_NC_filtered['Invoice date'] <= end_date)
]
# Apply program-specific filtering
df_Historic_dashboard_filtered = df_Historic_dashboard_filtered[df_Historic_dashboard_filtered['Program'] == selected_program]
df_Priority_dashboard_filtered = df_Priority_dashboard_filtered[df_Priority_dashboard_filtered['Program'] == selected_program]
df_Historic_NC_filtered = df_Historic_NC_filtered[df_Historic_NC_filtered['Program'] == selected_program]
df_Backlog_filtered = df_Backlog_filtered[df_Backlog_filtered['Program'] == selected_program]
df_Snapshot_KPI_filtered = df_Snapshot_KPI_filtered[
(df_Snapshot_KPI_filtered['Program'] == selected_program) &
(df_Snapshot_KPI_filtered['Shipped'] > 0)
]
df_SciformaReport_filtered = df_SciformaReport_filtered[df_SciformaReport_filtered['Program'] == selected_program]
df_SciformaReport_Team_filtered = df_SciformaReport_Team_filtered[df_SciformaReport_Team_filtered['Program'] == selected_program]
df_production_wip_filtered = df_production_wip_filtered[df_production_wip_filtered['Program'] == selected_program]
df_Historic_complete_filtered = df_Historic_complete_filtered[df_Historic_complete_filtered['Program'] == selected_program]
df_AccessDB_filtered = df_AccessDB_filtered[df_AccessDB_filtered['Program'] == selected_program]
# Handle empty df_AccessDB_filtered case
if df_AccessDB_filtered.empty:
print(f"⚠️ No AccessDB data found for program: {selected_program}. Proceeding with empty DataFrame.")
df_AccessDB_filtered = pd.DataFrame() # or keep as-is if it’s already empty
# Update span_report_historic_dashboard based on filtered historic data
if not df_Historic_dashboard_filtered.empty:
older_date = df_Historic_dashboard_filtered['Invoice date'].min()
recent_date = df_Historic_dashboard_filtered['Invoice date'].max()
older_date_str = older_date.strftime('%m/%d/%Y')
recent_date_str = recent_date.strftime('%m/%d/%Y')
span_report_historic_dashboard = f"{older_date_str} - {recent_date_str}"
else:
span_report_historic_dashboard = "No data available after filtering"
# Merge df_Priority_dashboard_filtered with filtered historic data
df_Priority_dashboard_filtered = pd.merge(
df_Priority_dashboard_filtered,
df_Historic_dashboard_filtered[['Pty Indice', 'Sales USD', 'Qty Shipped']],
on='Pty Indice',
how='left'
)
df_Priority_dashboard_filtered.fillna(0, inplace=True)
df_Priority_dashboard_filtered['Qty Shipped'] = df_Priority_dashboard_filtered['Qty Shipped'].astype(int)
df_Priority_dashboard_filtered['Shipped'] = df_Priority_dashboard_filtered['Shipped'].astype(int)
# Apply toggle filters
df_Historic_dashboard_filtered = filter_dashboard(
df_Historic_dashboard_filtered, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
df_Priority_dashboard_filtered = filter_dashboard(
df_Priority_dashboard_filtered, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
df_Historic_NC_filtered = filter_dashboard(
df_Historic_NC_filtered, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
df_Backlog_filtered = filter_dashboard(
df_Backlog_filtered, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
df_Historic_complete_filtered = filter_dashboard(
df_Historic_complete_filtered, toggle_lightplate.value, toggle_others.value, 'Product Category'
)
# Update plots and indicators
return update_plots_and_indicators(
df_Historic_dashboard_filtered, df_Priority_dashboard_filtered, df_Historic_NC_filtered,
df_Backlog_filtered, df_Snapshot_KPI_filtered, selected_program, df_SciformaReport_filtered, df_SciformaReport_Team_filtered, df_production_wip_filtered, df_Historic_complete_filtered, df_AccessDB_filtered
)
#03/28 updated
# Attach the update function to the program selection widget
program_widget_List.param.watch(update_data_dashboard, 'value')
#-------------------------------------------------------------------------------------------------------------------
# updated 03/28 and 04/01
# Function to update the plots and indicators
# Function to update the plots and indicators
def update_plots_and_indicators(df_Historic_dashboard_filtered, df_Priority_dashboard_filtered, df_Historic_NC_filtered, df_Backlog_filtered, df_Snapshot_KPI_filtered, selected_program, df_SciformaReport_filtered, df_SciformaReport_Team_filtered, df_production_wip_filtered, df_Historic_complete_filtered, df_AccessDB_filtered):
# Create date of report
date_report = pd.to_datetime(file_date)
formatted_date = date_report.strftime('%m-%d-%Y')
# Create figures and indicators
yoy_sales_figure = create_yoy_sales_figure(df_Historic_dashboard_filtered, selected_program, year_color_map)
yoy_shipments_figure = create_yoy_shipments_figure(df_Historic_dashboard_filtered, selected_program, year_color_map)
#customers_shipment, customers_sales = create_customers_figure(df_Priority_dashboard_filtered, logo_mapping, logo_offset) # 10/21
customers_shipment, customers_sales = create_customers_figure(df_Priority_dashboard_filtered, logo_mapping, df_Historic_dashboard_filtered, selected_program)
yearly_metrics_indicator = create_yearly_metrics_indicator(df_Historic_dashboard_filtered)
since_inception_indicator = create_since_inception_indicator(df_Historic_dashboard_filtered, df_Snapshot_KPI_filtered)
monthly_metrics_indicator = create_monthly_metrics_indicator(df_Historic_dashboard_filtered)
# 02/27 - Create the new yearly sales and shipment card
yearly_sales_shipment_layout = create_yearly_sales_shipment_card(df_Historic_dashboard_filtered)
# 03/04 - Create the yearly RMA card
#yearly_rma_card = create_yearly_rma_card(df_Historic_NC_filtered, year_color_map)
yearly_sales_shipment_data = calculate_yearly_sales_shipment(df_Historic_dashboard_filtered) # New 03/17
yearly_rma_card = create_yearly_rma_card(df_Historic_NC_filtered, year_color_map, yearly_sales_shipment_data) # Update 03/17
# 03/04 - Create the monthly RMA graph
monthly_rma_graph = create_card_graph_RMA(df_Historic_NC_filtered, year_color_map)
# 03/19 - Create the new backlog card
backlog_card = create_backlog_card(df_Backlog_filtered, year_color_map, file_date)
# 03/20 - Create the since inception note
note_since_inception = pn.pane.HTML(
create_note_since_inception(df_Snapshot_KPI_filtered), # Call the function to generate the HTML note
#sizing_mode='stretch_width'
)
#03/25 - Graph sales and fees
sales_fees_graph = create_monthly_sales_fees_graph(df_Historic_dashboard_filtered, year_color_map, selected_program)
# 03/27 - Create the new critical assemblies card
critical_assemblies_layout = create_critical_assemblies_card(df_Backlog_filtered, file_date, selected_program)
# 03/28 - Create the Monthly NRE fees Graph
NRE_fees_graph = create_monthly_nre_fees_graph(df_Historic_dashboard_filtered, df_SciformaReport_filtered, df_AccessDB_filtered, year_color_map, selected_program)
# 04/01 - Create the MMonthly Engineering Hours Consumed per Team
Engineering_hours_graph = create_monthly_engineering_hours_graph(df_SciformaReport_Team_filtered, selected_program)
# Update 04/09 - Create the Monthly Engineering Hours
Engineering_cumulative_hours_graph = create_monthly_cumulative_engineering_hours_with_ECO(df_SciformaReport_filtered, df_AccessDB_filtered, selected_program, year_color_map)
#04/15 updated - Create Engineering_monthly_ECO_KPI
#Engineering_monthly_ECO_KPI_Graph = create_engineering_monthly_ECO_KPI_Graph(df_AccessDB_filtered, selected_program)
# Conditionally create ECO KPI graph only for '1stTB'
if selected_program == '1stTB':
Engineering_monthly_ECO_KPI_Graph = create_engineering_monthly_ECO_KPI_Graph(df_AccessDB_filtered, selected_program)
else:
Engineering_monthly_ECO_KPI_Graph = pn.pane.HTML("<h3>ECO KPI Graph only available for 1stTB</h3>")
#04/04 - Create Production_wip_dataframe
Production_wip_dataframe = create_wip_dataframe_dashboard(df_production_wip_filtered, df_Backlog_filtered, selected_program, df_Historic_dashboard_filtered)
#04/04 - Create Heatmap
Sciforma_Heatmap = create_monthly_engineering_hours_heatmap(df_SciformaReport_Team_filtered, selected_program)
#04/10 - Create create_NC_received_per_assembly dataframe
NC_received_per_assembly = create_NC_received_per_assembly(df_Historic_complete_filtered, selected_program)
#05/16 - create_FTB_new_per_customer
FTB_new_per_customer = create_FTB_new_per_customer(df_AccessDB_filtered, selected_program, logo_mapping)
# Create and display the cards with specified styles
card_yoy_sales = create_card_indicators("Sales - YoY Sales", yoy_sales_figure, card_styles["YoY Sales"])
card_yoy_shipments = create_card_indicators("Sales - YoY Shipments", yoy_shipments_figure, card_styles["YoY Shipments"])
card_customers_shipment = create_card_indicators(f"Sales - Total Shipment per customer - [{span_report_historic_dashboard}]", customers_shipment, card_styles["Total Shipment per customer"])
card_customers_sales = create_card_indicators(f"Sales - Total sales per customer - [{span_report_historic_dashboard}]", customers_sales, card_styles["Total sales per customer"])
card_yearly_metrics = create_card_indicators("Sales - Beginning current year up to today vs. same period year prior", yearly_metrics_indicator, card_styles["Yearly KPI"])
card_since_inception = create_card_indicators(f"Sales - Cumulative KPI - [{span_report_historic_dashboard}]", since_inception_indicator, card_styles["Cumulative KPI"])
card_monthly_metrics = create_card_indicators("Sales - Monthly KPI, Previous month vs. two month prior", monthly_metrics_indicator, card_styles["Monthly KPI"])
# 02/27 - Create the new yearly sales and shipment card
card_yearly_sales_shipment = create_card_indicators("Sales - Yearly KPI, Sales and Shipment", yearly_sales_shipment_layout, card_styles["Yearly Sales and Shipment"])
# 03/04 - Create the yearly RMA card
card_yearly_RMA = create_card_indicators("Quality - Yearly KPI", yearly_rma_card, card_styles["Yearly RMA"])
# 03/04 - Create the yearly RMA graph
card_monthly_RMA = create_card_indicators("Quality - Monthly KPI", monthly_rma_graph, card_styles["Monthly RMA"])
# 03/19
card_backlog = create_card_indicators("Backlog - Yearly KPI", backlog_card, card_styles["Yearly KPI - Backlog"])
# 03/25 - Create the card using your defined styles
sales_fees_card = create_card_indicators("Sales - Monthly Sales and Fees", sales_fees_graph, card_styles["Monthly Sales and Fees"])
# 03/27 - Create the new critical assemblies card
card_critical_assemblies = create_card_indicators("Backlog - Most Critical Assemblies", critical_assemblies_layout, card_styles["Backlog - Most Critical Assemblies"])
# 03/28 - Create the card for create_monthly_nre_fees_graph
card_NRE_Grah = create_card_indicators("Engineering - Monthly NRE Fees", NRE_fees_graph, card_styles["Monthly NRE Fees"])
# 04/01 - Create the card for create_monthly_engineering_hours_graph
card_EngineeringHours_Graph = create_card_indicators("Engineering - Monthly NRE Engineering Hours Consumed per Team", Engineering_hours_graph, card_styles["Monthly Engineering Hours Consumed per Team"])
# 04/02 - Create the card for create_monthly_engineering_hours_graph
card_EngineeringHours_Cumulative_Graph = create_card_indicators("Engineering - Monthly NRE Engineering Cumulative Hours Consumed", Engineering_cumulative_hours_graph, card_styles["Monthly Engineering Cumulative Hours Consumed"])
# 04/02 - Create the card for create_engineering_monthly_ECO_KPI
card_Engineering_monthly_ECO_KPI_Graph = create_card_indicators("Engineering - Monthly ECO KPI", Engineering_monthly_ECO_KPI_Graph, card_styles["Monthly ECO KPI"])
#04/04 - Create the card for create_wip_dataframe_dashboard # 04/08 updated
card_wip_dataframe = create_card_indicators(f"Production - Top-Level assemblies on the floor at IDD in Redmond - [{formatted_date}]", Production_wip_dataframe, card_styles["Production - Assemblies on the floor at IDD in Redmond"])
#04/07 - Create card heatmap
Card_Sciforma_Heatmap = create_card_indicators("Engineering - Heatmap", Sciforma_Heatmap, card_styles["Engineering - Heatmap"])
#04/10 - create card for
card_RMA_Received_per_assembly = create_card_indicators("Quality - RMA Received per Assembly", NC_received_per_assembly, card_styles["Quality - Quality - RMA Received per Assembly"])
#05/16 - Create for new Production Introduction
card_New_FTB = create_card_indicators("Engineering - New FTB per customer", FTB_new_per_customer, card_styles["Engineering - New Product Introduction"])
#05/16
dashboard_layout = pn.Row(
# Left column
pn.Column(
pn.Row(card_monthly_metrics, pn.Spacer(width=10), card_yearly_metrics, pn.Spacer(width=10), card_since_inception),
pn.Row(note_since_inception) if selected_program != '1stTB' else pn.Spacer(height=150),
pn.Spacer(height=20),
pn.Row(card_yearly_sales_shipment, pn.Spacer(width=10), card_yearly_RMA, pn.Spacer(width=10), card_backlog),
pn.Spacer(height=150),
pn.Row(card_yoy_sales, pn.Spacer(width=10), card_yoy_shipments),
pn.Spacer(height=30),
pn.Row(card_NRE_Grah, pn.Spacer(width=10), sales_fees_card), # card_EngineeringHours_Cumulative_Graph to be included
pn.Spacer(height=30+150), # 04/14 - Additionnal space need for the html text
pn.Row(card_EngineeringHours_Cumulative_Graph, pn.Spacer(width=10), Card_Sciforma_Heatmap),
pn.Spacer(height=30),
pn.Row(card_EngineeringHours_Graph),
pn.Spacer(height=30),
# Conditionally display card_monthly_RMA for all program exept '1stTB'
#pn.Row(card_Engineering_monthly_ECO_KPI_Graph if selected_program == '1stTB' else card_monthly_RMA, pn.Spacer(width=10), card_RMA_Received_per_assembly if selected_program != '1stTB' else None), #04/18
# ------ 05/16 ---------------
pn.Row(
card_Engineering_monthly_ECO_KPI_Graph if selected_program == '1stTB' else card_monthly_RMA,
pn.Spacer(width=10),
card_New_FTB if selected_program == '1stTB' else card_RMA_Received_per_assembly,
),
#--------------------------------------------------
pn.Spacer(height=30),
pn.Row(card_customers_shipment, pn.Spacer(width=10), card_customers_sales),
#-------- Only for program '1stTB' ----------------
#pn.Spacer(height=30), # 05/16
#pn.Row(card_New_FTB, , pn.Spacer(width=10), None), # 05/16
#--------------------------------------------------
pn.Spacer(height=30),
pn.Row(card_wip_dataframe),
width=2000,
height=850,
sizing_mode='fixed'
),
# Right column
pn.Column(
pn.Spacer(height=5),
card_critical_assemblies,
width=250,
height=850,
margin=(0, 0, 0, 0),
sizing_mode='fixed'
),
width=2400,
height=850,
sizing_mode='fixed',
margin=(0, 0, 0, 10)
)
return dashboard_layout
# 03/08 updated
# Manually trigger the first update to populate the dashboard initially
dashboard_layout = update_data_dashboard(None, file_date) # Call the function directly to get the initial layout
# Trigger the initial filter application when the dashboard loads
apply_filters(None)
#//////////////////////////////////////////////////////////////
###############################################################
# Creating Saving buttons
###############################################################
#//////////////////////////////////////////////////////////////
#---------------------------------------------------------------------
# Save the full interactive dashboard with server-side saving
#---------------------------------------------------------------------
#04/10WIP
def save_full_dashboard(event):
timestamp = datetime.now().strftime("%m-%d-%Y")
save_dashboard_button.loading = True
# Declare global variables for watchers only
global program_watcher, lightplate_watcher, others_watcher
# Initialize watchers as None if they don't exist
if 'program_watcher' not in globals():
program_watcher = None
if 'lightplate_watcher' not in globals():
lightplate_watcher = None
if 'others_watcher' not in globals():
others_watcher = None
# Define local_file_date explicitly as today’s date, no global check
local_file_date = pd.to_datetime(datetime.now().strftime("%m-%d-%Y"))
print(f"local_file_date set in save_full_dashboard: {local_file_date}")
try:
# Verify required global variables exist
required_globals = [
'program_widget_List',
'toggle_lightplate',
'toggle_others',
'unique_programs_List',
'cover_dashboard'
]
missing_vars = [var for var in required_globals if var not in globals()]
if missing_vars:
raise NameError(f"Required global variables not defined: {', '.join(missing_vars)}")
if not unique_programs_List:
raise ValueError("No programs available in unique_programs_List")
# Setup save directory and timestamp
base_filename = f"Full_dashboard_{timestamp}"
save_dir = os.path.join(os.getcwd(), "dashboards")
os.makedirs(save_dir, exist_ok=True)
toggle_combinations = [
(True, True),
(True, False),
(False, True),
(False, False)
]
saved_files = []
total_combinations = len(unique_programs_List) * len(toggle_combinations)
print(f"Generating {total_combinations} dashboard variations...")
# Temporarily disable existing watchers
for watcher, widget in [
(program_watcher, program_widget_List),
(lightplate_watcher, toggle_lightplate),
(others_watcher, toggle_others)
]:
if watcher is not None:
try:
widget.param.unwatch(watcher)
print(f"Detached watcher for {widget.name}")
except Exception as e:
print(f"Warning: Failed to remove watcher: {str(e)}")
try:
for program in unique_programs_List:
program_widget_List.value = program
print(f"Set program_widget_List.value to: {program}")
for lightplate_state, others_state in toggle_combinations:
toggle_lightplate.value = lightplate_state
toggle_others.value = others_state
print(f"Set toggle_lightplate.value to: {lightplate_state}, toggle_others.value to: {others_state}")
# Manually update the dashboard with local_file_date
new_layout = update_data_dashboard(None, local_file_date)
cover_dashboard[-1] = new_layout
current_layout = cover_dashboard[-1]
# Create static title
toggle_info = f"Lightplate: {'On' if lightplate_state else 'Off'}, Others: {'On' if others_state else 'Off'}"
static_title = pn.pane.HTML(f"""
<div style='background-color: {font_top_color}; width: 100%; padding: 10px;'>
<h1 style='font-size: 24px; color: white; text-align: left; margin: 0;'>
{program} Dashboard - {toggle_info}
</h1>
</div>
""", sizing_mode='stretch_width')
# Define widget section (static copy)
widget_section = pn.Row(
pn.widgets.Select(name="Programs", value=program, options=program_widget_List.options, disabled=True),
pn.Spacer(width=50),
pn.Column(
pn.Spacer(height=15),
pn.Row(
pn.widgets.Toggle(name="Lightplate", value=lightplate_state, disabled=True),
pn.Spacer(width=15),
pn.widgets.Toggle(name="Others", value=others_state, disabled=True),
pn.Spacer(width=10),
pn.pane.Markdown("Click to <span style='color: #226AB0;'><b>Include</b></span> / <span style='color: #D9D9D9;'><b>Exclude</b></span> from the Dashboard")
)
),
sizing_mode='stretch_width'
)
# Build static layout
static_layout = pn.Column(
static_title,
pn.layout.Divider(margin=(-10, 0, 0, 0)),
pn.Spacer(height=15),
widget_section,
pn.Spacer(height=20),
pn.layout.Divider(margin=(0, 0, -10, 0)),
current_layout,
sizing_mode='stretch_width'
)
# Save the static layout
filename = f"{base_filename}_{program}_L{'on' if lightplate_state else 'off'}_O{'on' if others_state else 'off'}.html"
save_path = os.path.join(save_dir, filename)
try:
static_layout.save(
save_path,
title=f"{program} Dashboard",
embed=False
)
if not os.path.exists(save_path):
raise IOError(f"File not created: {save_path}")
saved_files.append(filename)
print(f"Saved: {filename}")
except Exception as e:
print(f"Failed to save {filename}: {str(e)}")
continue
finally:
# Reattach watchers
try:
program_watcher = program_widget_List.param.watch(update_cards, 'value')
lightplate_watcher = toggle_lightplate.param.watch(update_cards, 'value')
others_watcher = toggle_others.param.watch(update_cards, 'value')
print("Watchers reattached successfully")
except Exception as e:
print(f"Warning: Failed to reattach watchers: {str(e)}")
success_msg = f"Saved {len(saved_files)} dashboard variations in {save_dir}"
if hasattr(pn.state, 'notifications') and pn.state.notifications:
pn.state.notifications.success(success_msg)
else:
print(success_msg)
except Exception as e:
error_msg = f"❌ Save failed: {str(e)}"
print(error_msg)
if hasattr(pn.state, 'notifications') and pn.state.notifications:
pn.state.notifications.error(error_msg)
finally:
save_dashboard_button.loading = False
#---------------------------------------------------------------------
# Save the current view as a static snapshot with server-side saving
#---------------------------------------------------------------------
def save_current_view(event):
save_view_button.loading = True
try:
selected_program = program_widget_List.value
if not selected_program:
raise ValueError("No program selected.")
# Get the current dashboard layout
current_layout = cover_dashboard[-1] # Last item is dashboard_layout
# Create the static title
toggle_info = f"Lightplate: {'On' if toggle_lightplate.value else 'Off'}, Others: {'On' if toggle_others.value else 'Off'}"
static_title = pn.pane.HTML(f"""
<div style='background-color: {font_top_color}; width: 100%; padding: 10px; box-sizing: border-box;'>
<h1 style='font-size: 24px; color: white; text-align: left; margin: 0;'>Transfer Project Dashboard - {selected_program}</h1>
</div>
""", sizing_mode='stretch_width')
# Define the widget section to match cover_dashboard
widget_section = pn.Row(
program_widget_List,
pn.Spacer(width=50),
pn.Column(
pn.Spacer(height=15),
pn.Row(
toggle_lightplate,
pn.Spacer(width=15),
toggle_others,
pn.Spacer(width=10),
pn.pane.Markdown("Click to <span style='color: #226AB0;'><b>Include</b></span> / <span style='color: #D9D9D9;'><b>Exclude</b></span> from the Dashboard")
)
),
sizing_mode='stretch_width'
)
# Redefine static_layout to include the widget section
static_layout = pn.Column(
static_title,
pn.layout.Divider(margin=(-10, 0, 0, 0), sizing_mode='stretch_width'),
pn.Spacer(height=15),
widget_section, # Include the widget section
pn.Spacer(height=20),
pn.layout.Divider(margin=(0, 0, -10, 0)),
current_layout,
sizing_mode='stretch_width'
)
# Save the static layout
timestamp = datetime.now().strftime("%m-%d-%Y")
filename = f"{selected_program}_Dashboard_{timestamp}.html"
save_path = os.path.join(os.getcwd(), filename)
static_layout.save(save_path, title=f"{selected_program} Dashboard", embed=False)
if hasattr(pn.state, 'notifications') and pn.state.notifications:
pn.state.notifications.success(f"Current view saved: {filename}")
except Exception as e:
error_msg = f"❌ Save failed: {str(e)}"
print(error_msg)
if hasattr(pn.state, 'notifications') and pn.state.notifications:
pn.state.notifications.error(error_msg)
finally:
save_view_button.loading = False
# Define buttons
save_dashboard_button = pn.widgets.Button(
name="Save Dashboard",
button_type="default",
styles={
'background-color': '#D3D3D3',
'color': 'black',
'border': '1px solid #A9A9A9',
'margin': '5px',
'padding': '5px 10px'
}
)
save_view_button = pn.widgets.Button(
name="Save Current View",
button_type="default",
styles={
'background-color': '#006400',
'color': 'white',
'border': '1px solid #004d00',
'margin': '5px',
'padding': '5px 10px'
}
)
# Attach event handlers
save_dashboard_button.on_click(save_full_dashboard)
save_view_button.on_click(save_current_view)
#//////////////////////////////////////////////////////////////
###############################################################
# Creating the overall layout with proper spacing and alignment
###############################################################
#//////////////////////////////////////////////////////////////
# Create the title section with buttons
title_section = pn.Row(
pn.pane.HTML(f"""
<div style='background-color: {font_top_color}; width: 100%; padding: 10px; box-sizing: border-box;'>
<h1 style='font-size: 24px; color: white; text-align: left; margin: 0;'>Transfer Project Dashboard</h1>
</div>
""", sizing_mode='stretch_width'),
pn.Spacer(sizing_mode='stretch_width'), # This will push the buttons to the right
save_dashboard_button,
pn.Spacer(width=10),
save_view_button,
align='center', # Vertically center the title and buttons
sizing_mode='stretch_width',
styles={'background': font_top_color}
)
# Main dashboard layout
cover_dashboard = pn.Column(
title_section,
pn.layout.Divider(margin=(-10, 0, 0, 0), sizing_mode='stretch_width'),
pn.Spacer(height=15),
pn.Row(
program_widget_List, # Existing widget layout
pn.Spacer(width=50), # Space before the toggles
pn.Column( # Use Column to stack Spacer and Row for toggles
pn.Spacer(height=15), # Empty line above the toggle buttons
pn.Row(
toggle_lightplate, # Toggle buttons in the same row
pn.Spacer(width=15), # Space between the buttons
toggle_others,
pn.Spacer(width=10),
pn.pane.Markdown("""
<span style='color: #000000;'>Click to </span><span style='color: #226AB0;'><b>Include</b></span><span style='color: #000000;'> / </span><span style='color: #D9D9D9;'><b>Exclude</b></span><span style='color: #000000;'> from the Dashboard</span>
""")
)
),
sizing_mode='stretch_width' # Ensure the row stretches to fill the width
),
pn.Spacer(height=20),
pn.layout.Divider(margin=(0, 0, -10, 0)),
dashboard_layout,
sizing_mode='stretch_width'
)
# Add CSS styling
pn.extension(notifications=True, raw_css=["""
.bk-root {
background-color: white !important;
}
button.bk-btn[data-name="Save Dashboard"] {
background-color: #D3D3D3 !important;
color: black !important;
border: 1px solid #A9A9A9 !important;
padding: 5px 10px !important;
min-width: 120px !important;
transition: background-color 0.2s ease !important;
}
button.bk-btn[data-name="Save Dashboard"]:hover {
background-color: #C0C0C0 !important;
}
button.bk-btn[data-name="Save Current View"] {
background-color: #006400 !important;
color: white !important;
border: 1px solid #004d00 !important;
padding: 5px 10px !important;
min-width: 120px !important;
transition: background-color 0.2s ease !important;
}
button.bk-btn[data-name="Save Current View"]:hover {
background-color: #008000 !important;
}
.bk-btn.bk-btn-toggle {
margin: 0 !important;
}
"""])
#-------------------------------------------------------------------------------------------------------------------
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# Tab |Clear to Build summary|
##############################################################################################################################
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Load the data
df_Summary_Database = pd.read_excel(input_file_formatted, sheet_name='Summary', index_col=False)
# Create 'Program' within df_Summary_Database
df_Summary_Database['Program'] = df_Summary_Database['Pty Indice'].map(program_mapping)
# Clean unnecessary columns (do not drop 'IDD Top Level' and 'SEDA Top Level' yet)
#columns_to_drop = ['BOM_Index', 'Is_Make_Part', 'Parent IDD', 'Child IDDs', 'Top Level sharing Components']
columns_to_drop = ['BOM_Index', 'Is_Make_Part', 'Parent IDD', 'Child IDDs']
df_Summary_Database = df_Summary_Database.drop(columns=[col for col in columns_to_drop if col in df_Summary_Database.columns])
# Define max character length for the column 'Top Level sharing Components'
max_length = 150 # Adjust as needed
# Truncate the 'Top Level sharing Components' column
if 'Top Level sharing Components' in df_Summary_Database.columns:
df_Summary_Database['Top Level sharing Components'] = df_Summary_Database['Top Level sharing Components'].astype(str).apply(
lambda x: (x[:max_length] + '...') if len(x) > max_length else x
)
# Apply Phase 4-5 grouping to df_Summary_Database
if 'Program' in df_Summary_Database.columns:
phase_mask = (
df_Summary_Database['Program'].isin(['Phase 4', 'Phase 5']) &
~df_Summary_Database['Pty Indice'].str.contains('Phase5', na=False)
)
df_Summary_Database.loc[phase_mask, 'Program'] = 'Phase 4-5'
# Handle text formatting
acronyms = ['EDA', 'PCB', 'PWB', 'CPA', 'CPSL', 'ISP', 'TBD']
df_Summary_Database['Supplier'] = df_Summary_Database['Supplier'].astype(str).apply(lambda x: title_with_acronyms(x, acronyms))
df_Summary_Database['Description'] = df_Summary_Database['Description'].astype(str).apply(lambda x: title_with_acronyms(x, acronyms))
# Convert 'Max Qty Top-Level' to integer, coercing errors to NaN
df_Summary_Database['Max Qty Top-Level'] = pd.to_numeric(df_Summary_Database['Max Qty Top-Level'], errors='coerce')
# Convert 'Max Qty (GS)' to numeric and coerce errors to NaN
df_Summary_Database['Max Qty (GS)'] = pd.to_numeric(df_Summary_Database['Max Qty (GS)'], errors='coerce')
df_Summary_Database['Max Qty (GS)'] = df_Summary_Database['Max Qty (GS)'].apply(lambda x: '' if pd.isna(x) else int(x))
# If 'Level' column exists, set 'Qty On Hand' to empty string where Level == 0
if 'Level' in df_Summary_Database.columns and 'Qty On Hand' in df_Summary_Database.columns:
df_Summary_Database.loc[df_Summary_Database['Level'] == 0, 'Qty On Hand'] = ''
# Fill NaN values with an empty string and convert the column to string type
df_Summary_Database['Comment'] = df_Summary_Database['Comment'].fillna('').astype(str)
df_Summary_Database['Supplier'] = df_Summary_Database['Supplier'].fillna('').astype(str)
df_Summary_Database['Description'] = df_Summary_Database['Description'].fillna('').astype(str)
# Replace NaN, 'Nan', and 'nan' with an empty string
df_Summary_Database = df_Summary_Database.replace([np.nan, 'Nan', 'nan'], '')
#---------------------------------
# Styling Function
#---------------------------------
def style_summary_table(df):
if df.empty:
return pd.DataFrame().style # Return empty style if DataFrame is empty
styler = df.style.hide(axis='index') # Hide the index column
# Apply dark blue (#5B9BD5) to entire rows where Level = 0, except 'Max Qty Top-Level'
# Apply light blue (#00B0F0) to 'Max Qty Top-Level' column where Level = 0
if 'Level' in df.columns:
styler = styler.apply(
lambda row: [
f'background-color: #5B9BD5; color: white; font-weight: bold; text-align: center;'
if row['Level'] == 0 and col != 'Max Qty Top-Level'
else f'background-color: #00B0F0; color: white; font-weight: bold; text-align: center;'
if row['Level'] == 0 and col == 'Max Qty Top-Level'
else ''
for col in df.columns
],
axis=1
)
# Apply base styles: Center align all text
styler = styler.set_properties(**{
'text-align': 'center',
'vertical-align': 'middle'
})
# Override alignment for 'Top Level sharing Components' and 'Comment' columns
if 'Top Level sharing Components' in df.columns:
styler = styler.set_properties(subset=['Top Level sharing Components'], **{
'text-align': 'left', # Left-align this column
'vertical-align': 'middle'
})
if 'Comment' in df.columns:
styler = styler.set_properties(subset=['Comment'], **{
'text-align': 'left', # Left-align this column
'vertical-align': 'middle'
})
# Zero quantity styling
if 'Max Qty (GS)' in df.columns:
red_mask = df['Max Qty (GS)'] == 0
styler = styler.apply(
lambda row: ['background-color: #FFC7CE' if red_mask.loc[row.name] else '' for _ in row],
axis=1
)
# Supplier-based formatting
supplier_colors = {
'Make Part (Phantom)': '#D9E1F2',
'Make Part': '#D9E1F2',
'Floor Stock Item': '#E0E0E0',
'Make Part CUU': '#CCCCFF'
}
for supplier_pattern, color in supplier_colors.items():
mask = df['Supplier'].str.strip().str.lower() == supplier_pattern.strip().lower()
styler = styler.apply(
lambda row, mask=mask, color=color:
[f'background-color: {color}' if mask.loc[row.name] else '' for _ in row],
axis=1
)
# Status formatting
status_colors = {
'Clear-to-Build': '#C6EFCE',
'Completed - No Backlog': '#6FAC46',
'Not completed - No Backlog': '#ED7D31',
'Shortage': '#FFC7CE'
}
if 'Top-Level Status' in df.columns:
styler = styler.map(
lambda v: f'background-color: {status_colors.get(v, "")}',
subset=['Top-Level Status']
)
# Highlight 'Max Qty Top-Level' in light blue where 'Critical' is True
if 'Critical' in df.columns:
def highlight_critical(row):
return [
'background-color: #00B0F0; color: white' if col == 'Max Qty Top-Level' and row['Critical'] else ''
for col in df.columns
]
styler = styler.apply(highlight_critical, axis=1)
# Level-based coloring
if 'Level' in df.columns:
level_colors = {
0: '#63BE7B', 1: '#A2C075', 2: '#FFEB84',
3: '#FFD166', 4: '#F88E5B', 5: '#F8696B', 6: '#8B0000'
}
styler = styler.map(
lambda v: f'background-color: {level_colors.get(v, "transparent")}',
subset=['Level']
)
return styler
#---------------------------------
# Default values
#---------------------------------
# Default values
default_summary_program = 'Phase 4-5'
default_summary_priority = 6
default_summary_indice = 'P6'
# Filter functions
def filter_summary_priorities(program):
return sorted(df_Summary_Database[df_Summary_Database['Program'] == program]['Priority'].unique().tolist())
def filter_summary_indices(priority):
return sorted(df_Summary_Database[df_Summary_Database['Priority'] == priority]['Pty Indice'].unique().tolist())
# Initialize program widget
program_widget_summary = pn.widgets.Select(
name='Select Program',
options=sorted(df_Summary_Database['Program'].unique()),
value=default_summary_program
)
# Initialize priority widget with program dependency
priority_widget_summary = pn.widgets.Select(
name='Select Priority',
options=filter_summary_priorities(default_summary_program),
value=default_summary_priority
)
# Initialize indice widget with priority dependency
indice_widget_summary = pn.widgets.Select(
name='Select Pty Indice',
options=filter_summary_indices(default_summary_priority),
value=default_summary_indice
)
# Define the callback to update Priority widget when Program changes
def update_priority_widget(event):
selected_program = program_widget_summary.value
priority_widget_summary.options = filter_summary_priorities(selected_program)
if priority_widget_summary.options:
priority_widget_summary.value = priority_widget_summary.options[0]
# Define the callback to update Indice widget when Priority changes
def update_indice_widget(event):
selected_priority = priority_widget_summary.value
indice_widget_summary.options = filter_summary_indices(selected_priority)
if indice_widget_summary.options:
indice_widget_summary.value = indice_widget_summary.options[0]
# Link the Program widget to the callback
program_widget_summary.param.watch(update_priority_widget, 'value')
# Link the Priority widget to the callback
priority_widget_summary.param.watch(update_indice_widget, 'value')
#-------------------------------------------------------------------
# Define filtering widgets for 'IDD Top Level' and 'SEDA Top Level'
#-------------------------------------------------------------------label_idd_top_level = pn.pane.HTML('<b style="color:#2B70B3;">IDD Top Level Filter</b>')
label_seda_top_level = pn.pane.HTML('<b style="color:#2B70B3;">SEDA Top Level Filter</b>')
# Update 05/16
filters_top_level = {
'IDD Top Level': pn.widgets.TextInput(name='', placeholder='Enter IDD Top Level'),
'SEDA Top Level': pn.widgets.TextInput(name='', placeholder='Enter SEDA Top Level'),
}
# Create buttons for applying filters
filters_top_level_button = pn.widgets.Button(name='Apply Filters', button_type='primary')
reset_button = pn.widgets.Button(name='Reset Filters', button_type='danger')
# Set default value to None for all filter widgets
for widget in filters_top_level.values():
widget.value = ''
# Create the layout with labels, filter widgets, and buttons
filter_widgets_top_level = pn.Row(
pn.Column(label_idd_top_level, filters_top_level['IDD Top Level']),
pn.Column(label_seda_top_level, filters_top_level['SEDA Top Level']),
pn.Column(
pn.Spacer(height=25), # Spacer before the buttons
pn.Row(filters_top_level_button, reset_button)
)
)
# Define callback function for the Apply Filters button
def on_top_level_filter_button_click(event):
idd_top_level_filter = filters_top_level['IDD Top Level'].value
seda_top_level_filter = filters_top_level['SEDA Top Level'].value
# Filter the DataFrame based on the selected 'IDD Top Level' and 'SEDA Top Level'
filtered_df = df_Summary_Database.copy()
if idd_top_level_filter:
filtered_df = filtered_df[filtered_df['IDD Top Level'].str.contains(idd_top_level_filter, case=False, na=False)]
if seda_top_level_filter:
filtered_df = filtered_df[filtered_df['SEDA Top Level'].str.contains(seda_top_level_filter, case=False, na=False)]
# Update the widget values based on the filtered DataFrame
if not filtered_df.empty:
# Update program widget value (but keep original options)
program_widget_summary.value = filtered_df['Program'].iloc[0]
# Update priority widget value and options (but keep original options)
priority_options = sorted(filtered_df['Priority'].unique())
priority_widget_summary.value = filtered_df['Priority'].iloc[0]
# Update indice widget value and options (but keep original options)
indice_options = sorted(filtered_df['Pty Indice'].unique())
indice_widget_summary.value = filtered_df['Pty Indice'].iloc[0]
# Update the table
update_summary_table(event)
# Define the summary_html pane
summary_html = pn.pane.HTML(
styles={
'overflow-x': 'auto', # Enables horizontal scrolling
'overflow-y': 'hidden', # Prevents vertical scrolling
'margin': '15px 0',
'background': 'white',
'padding': '10px',
'border': 'none',
'display': 'block',
'width': '100%', # Ensures full width
'min-width': '1500px', # Sets a minimum width of 1500px
'max-width': '100%',
},
sizing_mode='stretch_width' # Expands width dynamically
)
#----------------------------------------------------------
# Unified update function
#----------------------------------------------------------
def update_summary_table(event):
try:
# Filter the DataFrame based on the selected program, priority, and indice
filtered_df = df_Summary_Database[
(df_Summary_Database.Program == program_widget_summary.value) &
(df_Summary_Database.Priority == priority_widget_summary.value) &
(df_Summary_Database['Pty Indice'] == indice_widget_summary.value)
]
# Drop 'IDD Top Level' and 'SEDA Top Level' columns at the very end
filtered_df = filtered_df.drop(columns=['IDD Top Level', 'SEDA Top Level', 'Program'], errors='ignore')
# Add these columns if they exist in your data
if 'Level' in filtered_df.columns:
filtered_df['Level'] = pd.to_numeric(filtered_df['Level'], errors='coerce').fillna(-1).astype(int)
# Apply styling to the filtered DataFrame
styled_table = style_summary_table(filtered_df)
# Hide the 'Critical' column in the styled table before rendering
if 'Critical' in filtered_df.columns:
styled_table = styled_table.hide(axis="columns", subset=['Critical'])
# Apply styles inside a scrollable div
summary_html.object = f"""
<div style='overflow-x: auto; overflow-y: hidden; width: 100%; min-width: 1500px; white-space: nowrap;'>
{styled_table.to_html(index=False)}
</div>
"""
except Exception as e:
print(f"Error updating table: {str(e)}")
summary_html.object = "<div>Error loading data</div>"
# Define callback function for the Reset Filters button
def on_reset_button_click(event):
# Reset the filter widgets to their default values
filters_top_level['IDD Top Level'].value = ''
filters_top_level['SEDA Top Level'].value = ''
# Reset the program, priority, and indice widgets to their default values and options
program_widget_summary.options = sorted(df_Summary_Database['Program'].unique())
program_widget_summary.value = default_summary_program
priority_widget_summary.options = filter_summary_priorities(default_summary_program)
priority_widget_summary.value = default_summary_priority
indice_widget_summary.options = filter_summary_indices(default_summary_priority)
indice_widget_summary.value = default_summary_indice
# Update the table
update_summary_table(event)
reset_button.on_click(on_reset_button_click)
# Define callback function for the Apply Filters button
def on_top_level_filter_button_click(event):
idd_top_level_filter = filters_top_level['IDD Top Level'].value
seda_top_level_filter = filters_top_level['SEDA Top Level'].value
# Filter the DataFrame based on the selected 'IDD Top Level' and 'SEDA Top Level'
filtered_df = df_Summary_Database.copy()
if idd_top_level_filter:
filtered_df = filtered_df[filtered_df['IDD Top Level'].str.contains(idd_top_level_filter, case=False, na=False)]
if seda_top_level_filter:
filtered_df = filtered_df[filtered_df['SEDA Top Level'].str.contains(seda_top_level_filter, case=False, na=False)]
# Update the program, priority, and indice widgets based on the filtered DataFrame
if not filtered_df.empty:
program_widget_summary.options = sorted(filtered_df['Program'].unique())
program_widget_summary.value = filtered_df['Program'].iloc[0]
priority_widget_summary.options = sorted(filtered_df['Priority'].unique())
priority_widget_summary.value = filtered_df['Priority'].iloc[0]
indice_widget_summary.options = sorted(filtered_df['Pty Indice'].unique())
indice_widget_summary.value = filtered_df['Pty Indice'].iloc[0]
# Update the table
update_summary_table(event)
# Link the buttons to their respective update functions
filters_top_level_button.on_click(on_top_level_filter_button_click)
# Set up final watchers
program_widget_summary.param.watch(update_summary_table, 'value')
priority_widget_summary.param.watch(update_summary_table, 'value')
indice_widget_summary.param.watch(update_summary_table, 'value')
# Initial update to populate the table
update_summary_table(None)
#----------------------------------------------------------
# 03/06 - Text above summary table
#----------------------------------------------------------
text_above_summary_table= (
f"This table is based on the tab |Summary| - <b>{formatted_date}</b>:<br>"
"▷ The PNs with a <b>Remain. crit. Qty</b> set to 0 are excluded from the table to reduce data processing.<br>"
)
#----------------------------------------------------------
# Final Layout
#----------------------------------------------------------
summary_tab = pn.Column(
pn.Row(
pn.pane.HTML(f"""
<div style='background-color:#4472C4; padding:10px'>
<h1 style='color:white; margin:0'>Clear to Build [{formatted_date}]</h1>
</div>
""", sizing_mode='stretch_width'),
sizing_mode='stretch_width'
),
pn.Row(
program_widget_summary,
priority_widget_summary,
indice_widget_summary,
sizing_mode='stretch_width'
),
filter_widgets_top_level, # Add the new filtering widgets
pn.layout.Divider(styles={'margin': '10px 0'}), # Removed border
text_above_summary_table,
summary_html,
sizing_mode='stretch_width',
)
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# 05/19 - Tab |Timeline|
##############################################################################################################################
import plotly.graph_objects as go
from dateutil.relativedelta import relativedelta
pn.extension('plotly') # <- required for UI
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Define the program selector widget with 'Phase 4-5' as default
program_widget_timeline = pn.widgets.Select(
name='Select Program',
options=sorted(df_Timeline['Program'].unique().tolist()),
value='Phase 4-5',
)
#---------------------------------------------------------
# Color fomratting for Gantt Generator
#---------------------------------------------------------
# Define colors for each category
category_colors = {
'Project': '#1f77b4',
'Documentation': '#9dcae6',
'Engineering FTB': '#9dcae6',
'Sourcing': '#ffc673',
'Production': '#9bccb5',
'Prototyping': '#e5b6a5',
'FAI': '#d1bbdf',
'Rototellite': '#a3d5d3',
'CPA': '#cfbcd5',
'ISP': '#b4d7c1',
}
category_background_colors = {
'Documentation': '#add8e6',
'Engineering FTB': '#add8e6',
'Sourcing': '#ffd6d1',
'Production': '#bfeceb',
'Project': '#87ceeb',
'Prototyping': '#ffe5d4',
'FAI': '#e6e6fa',
'Rototellite': 'white',
'CPA': 'white',
'ISP': 'white',
}
Dependency_arrow_colors = {
'Documentation': '#00DFDA',
'Engineering FTB': '#00DFDA',
'Sourcing': '#fa9324',
'Production': '#00ff7f',
'Project': '#87ceeb',
'FAI': '#771cb2',
'Prototyping': '#ff4500',
'Rototellite': '#00bfff',
'CPA': '#ff00ff',
'ISP': '#7fff00',
}
#--------------------------------------------------------
# Gantt Generator function
#--------------------------------------------------------
def create_gantt_chart(project_name: str, project_df: pd.DataFrame) -> go.Figure:
"""Generate a Gantt chart for a single project."""
# Clean and prepare data
project_df['Category'] = project_df['Category'].fillna('').str.strip()
project_df['Start'] = pd.to_datetime(project_df['Start'], errors='coerce')
project_df['End'] = pd.to_datetime(project_df['End'], errors='coerce')
project_df['Y_Label'] = project_df.apply(lambda x: f"{x['Task']}", axis=1)
project_df = project_df.dropna(subset=['Start', 'End']).reset_index()
if project_df.empty:
fig = go.Figure()
fig.update_layout(
title=dict(text=f"No Data for {project_name}", font=dict(size=20, color="#1f77b4"), x=0.04, xanchor='left'),
height=600,
width=2400,
plot_bgcolor='white',
paper_bgcolor='white',
margin=dict(l=150),
annotations=[dict(xref="paper", yref="paper", x=0.5, y=0.5, text=f"No valid data for {project_name}.", showarrow=False, font=dict(size=16, color="black"))]
)
return fig
# Determine category order based on latest end date
category_end_dates = project_df.groupby('Category')['End'].max().sort_values()
ordered_categories = category_end_dates.index.tolist()
gantt_data = []
y_labels = []
y_positions = []
category_ranges = {}
current_index = 0
for category in ordered_categories:
if category in project_df['Category'].unique():
y_labels.append(f"<b><span style='color:{category_colors.get(category, '#1f77b4')}'>{category.upper()}</span></b>")
y_positions.append(current_index)
current_index += 1
category_df = project_df[project_df['Category'] == category].sort_values('Start')
category_start_idx = current_index
for _, row in category_df.iterrows():
y_labels.append(f" {row['Y_Label']}")
y_positions.append(current_index)
percentage = 1.0
if isinstance(row['Description'], str) and any(c.isdigit() for c in row['Description']):
percentage = float(next((float(s.rstrip('%'))/100 for s in row['Description'].split() if s.replace('.','',1).isdigit() or s.endswith('%')), 1.0))
task_dict = dict(
Task=f" {row['Y_Label']}",
Start=row['Start'],
Finish=row['End'],
Resource=row['Category'],
Color=category_colors.get(row['Category'], '#1f77b4'),
Index=current_index,
IsMilestone=(row['End'] - row['Start']).days <= 0,
Description=row['Description'],
Dependencies=row['Dependencies'] if pd.notna(row['Dependencies']) else '',
OriginalIndex=row['index'],
ID=row['ID'],
Percentage=percentage,
Completion=row['% Completion'] if '% Completion' in row and pd.notna(row['% Completion']) else None,
Comment=row['Comment'] if pd.notna(row['Comment']) else ''
)
gantt_data.append(task_dict)
current_index += 1
category_end_idx = current_index - 0.5
category_ranges[category] = (category_start_idx - 0.5, category_end_idx)
y_labels = list(reversed(y_labels))
y_positions = list(reversed(y_positions))
for item in gantt_data:
item['Index'] = len(y_labels) - 1 - item['Index']
fig = go.Figure()
# Create bars for tasks
for task in gantt_data:
if not task['IsMilestone']:
completion = task['Completion'] if task['Completion'] is not None else 0.0
is_completed = isinstance(task['Description'], str) and 'completed' in task['Description'].lower()
total_duration = (task['Finish'] - task['Start']).total_seconds() * 1000
if is_completed or completion <= 0:
bar_color = '#00fa85' if is_completed else task['Color']
fig.add_trace(go.Bar(
x=[total_duration],
y=[task['Index']],
base=[task['Start'].timestamp() * 1000],
orientation='h',
marker=dict(color=bar_color, line=dict(color='black', width=1)),
width=0.6,
showlegend=False
))
else:
completed_duration = total_duration * min(completion, 1.0)
remaining_duration = total_duration - completed_duration
fig.add_trace(go.Bar(
x=[completed_duration],
y=[task['Index']],
base=[task['Start'].timestamp() * 1000],
orientation='h',
marker=dict(color=Dependency_arrow_colors.get(task['Resource'], '#00fa85'), line=dict(color='black', width=1)),
width=0.6,
showlegend=False,
name='Completed'
))
if remaining_duration > 0:
fig.add_trace(go.Bar(
x=[remaining_duration],
y=[task['Index']],
base=[task['Start'].timestamp() * 1000 + completed_duration],
orientation='h',
marker=dict(color=task['Color'], line=dict(color='black', width=1)),
width=0.6,
showlegend=False,
name='Remaining'
))
fig.update_layout(
barmode='stack',
yaxis=dict(ticktext=y_labels, tickvals=list(range(len(y_labels))), title='Tasks', tickfont=dict(size=10), showgrid=False, zeroline=False, range=[-0.5, len(y_labels) - 0.5])
)
if is_completed:
fig.add_annotation(x=task['Start'].timestamp() * 1000 + (total_duration / 2), y=task['Index'], text="Completed", showarrow=False, font=dict(size=9, color='black', family='Arial'), xanchor='center', yanchor='middle')
else:
display_text = f"{int(task['Percentage'] * 100)}%" if task['Percentage'] < 1.0 else task['Description']
fig.add_annotation(x=task['Start'].timestamp() * 1000 + (total_duration / 2), y=task['Index'], text=display_text, showarrow=False, font=dict(size=9, color='black', family='Arial'), xanchor='center', yanchor='middle')
fig.add_annotation(x=task['Finish'].timestamp() * 1000 + 500000, y=task['Index'], text="▶", showarrow=False, font=dict(size=12, color=Dependency_arrow_colors.get(task['Resource'], task['Color'])), xanchor='center', yanchor='middle')
def calculate_dynamic_offset(start, finish, chart_width_ms, percentage=0.005, min_offset_ms=1000000):
duration_ms = (finish - start).total_seconds() * 1000
chart_offset = chart_width_ms * percentage
return max(min_offset_ms, chart_offset)
def estimate_text_width_ms(text, chart_width_ms, chart_width_pixels=2400, avg_char_width_pixels=12):
text_length = len(str(text))
text_width_pixels = text_length * avg_char_width_pixels
ms_per_pixel = chart_width_ms / chart_width_pixels
return text_width_pixels * ms_per_pixel
base_x = task['Finish'].timestamp() * 1000
chart_start_ms = project_df['Start'].min().timestamp() * 1000
chart_end_ms = project_df['End'].max().timestamp() * 1000
chart_width_ms = chart_end_ms - chart_start_ms
completion_offset = calculate_dynamic_offset(task['Start'], task['Finish'], chart_width_ms)
padding_offset = calculate_dynamic_offset(task['Start'], task['Finish'], chart_width_ms, percentage=0.01)
arrow_buffer = 1000000
completion_x = base_x + completion_offset + arrow_buffer
completion_text = ""
has_completion = task['Completion'] is not None and task['Completion'] > 0
if has_completion:
completion_text = f"{int(task['Completion'] * 100)}%"
fig.add_annotation(x=completion_x, y=task['Index'], text=completion_text, showarrow=False, font=dict(size=12, color=Dependency_arrow_colors.get(task['Resource'], '#00fa85'), family='Arial', weight='bold'), xanchor='left', yanchor='middle')
if task['Comment']:
if has_completion:
completion_width_ms = estimate_text_width_ms(completion_text, chart_width_ms)
comment_x = completion_x + completion_width_ms + padding_offset
else:
comment_x = base_x + completion_offset + arrow_buffer
fig.add_annotation(x=comment_x, y=task['Index'], text=f"({task['Comment']})", showarrow=False, font=dict(size=9, color='black', family='Arial'), xanchor='left', yanchor='middle')
# Draw milestones
for task in gantt_data:
if task['IsMilestone']:
fig.add_trace(go.Scatter(
x=[task['Start'].timestamp() * 1000],
y=[task['Index']],
mode='markers',
marker=dict(symbol='star', size=15, color=task['Color'], line=dict(color='black', width=1)),
showlegend=False
))
fig.add_annotation(x=task['Start'].timestamp() * 1000 + 15000000, y=task['Index'] - 0.2, text=f"<b>{task['Task']} - {task['Start'].strftime('%m/%d')}</b>", showarrow=False, font=dict(size=8, color='black'), xanchor='left', yanchor='middle')
# Draw dependency arrows
for task in gantt_data:
if task['Dependencies']:
deps = [d.strip() for d in str(task['Dependencies']).split(',') if d.strip()]
for dep in deps:
try:
dep_id = int(float(dep)) if isinstance(dep, (float, str)) and dep != '' else None
if dep_id is not None:
dep_row = project_df[project_df['ID'] == dep_id].iloc[0] if not project_df[project_df['ID'] == dep_id].empty else None
if dep_row is not None:
dep_task = next((t for t in gantt_data if t['Task'] == f" {dep_row['Y_Label']}"), None)
if dep_task:
if task['IsMilestone']:
continue
triangle_x = task['Start'].timestamp() * 1000
vertical_x = triangle_x
dep_start_x = dep_task['Finish'].timestamp() * 1000
dep_y_index = dep_task['Index']
if dep_task['IsMilestone']:
task_name_parts = dep_task['Task'].lower().split()
corresponding_bar = None
for t in gantt_data:
if not t['IsMilestone'] and t['Resource'] == dep_task['Resource']:
t_name_parts = t['Task'].lower().split()
if any(part in t_name_parts for part in task_name_parts if part not in ['completed', 'shipped']):
if t['Finish'] <= dep_task['Start']:
corresponding_bar = t
break
if corresponding_bar:
dep_start_x = corresponding_bar['Finish'].timestamp() * 1000
dep_y_index = corresponding_bar['Index']
line_color = Dependency_arrow_colors.get(dep_task['Resource'], dep_task['Color'])
dash_pattern = "6,4"
line_width = 3
fig.add_shape(type="line", x0=dep_start_x, y0=dep_y_index, x1=vertical_x, y1=dep_y_index, line=dict(color=line_color, width=line_width, dash=dash_pattern), layer='below')
fig.add_shape(type="line", x0=vertical_x, y0=dep_y_index, x1=vertical_x, y1=task['Index'], line=dict(color=line_color, width=line_width, dash=dash_pattern), layer='below')
fig.add_annotation(x=triangle_x, y=task['Index'], text="▶", showarrow=False, font=dict(size=16, color='black'), xanchor='left', yanchor='middle')
fig.add_annotation(x=triangle_x, y=task['Index'], text="▶", showarrow=False, font=dict(size=14, color=line_color), xanchor='left', yanchor='middle')
except (ValueError, IndexError) as e:
print(f"Dependency error for {task['Task']} in {project_name}: {e}")
# Add category background rectangles
for category, (start_idx, end_idx) in category_ranges.items():
start_idx = len(y_labels) - 1 - start_idx
end_idx = len(y_labels) - 1 - end_idx
fig.add_shape(type="rect", x0=project_df['Start'].min().timestamp() * 1000, x1=project_df['End'].max().timestamp() * 1000, y0=start_idx, y1=end_idx, fillcolor=category_background_colors.get(category, 'white'), opacity=0.3, layer="below", line=dict(width=0))
# Draw today's date line
today = datetime.now()
fig.add_shape(type="line", x0=today.timestamp() * 1000, x1=today.timestamp() * 1000, y0=-0.5, y1=len(y_labels) - 0.5, line=dict(color="#006400", width=1, dash="dot"))
fig.add_annotation(x=today.timestamp() * 1000 + 500000, y=len(y_labels) - 0.5, text=f"<b>{today.strftime('%m/%d')}</b>", showarrow=False, font=dict(size=10, color="#006400"), xanchor='left', yanchor='top')
# Add vertical dashed lines every month
one_month = relativedelta(months=1)
start_date = project_df['Start'].min().replace(day=1)
end_date = project_df['End'].max()
current_date_iter = start_date
while current_date_iter <= end_date:
fig.add_shape(type="line", x0=current_date_iter.timestamp() * 1000, x1=current_date_iter.timestamp() * 1000, y0=-0.5, y1=len(y_labels) - 0.5, line=dict(color="lightgray", width=1, dash="4,6"), layer='below')
current_date_iter += one_month
current_date_iter = current_date_iter.replace(day=1)
fig.update_layout(
title=dict(text=f"<b>{project_name}</b> - Gantt Chart", font=dict(size=20, color="#1f77b4"), x=0.04, xanchor='left'),
xaxis=dict(type='date', tickformat='%b %d %Y', title='Date', tickfont=dict(size=10), showgrid=False, dtick="M1"),
yaxis=dict(ticktext=y_labels, tickvals=list(range(len(y_labels))), title='Tasks', tickfont=dict(size=10), showgrid=False),
height=1200, # Reduced height for better stacking
width=2400,
showlegend=False,
plot_bgcolor='white',
paper_bgcolor='white',
margin=dict(l=150)
)
return fig
#---------------------------------------------------------
# Timeline_chart function
#---------------------------------------------------------
@pn.depends(program=program_widget_timeline.param.value)
def Timeline_chart(program: str) -> pn.Column:
"""Generate Gantt charts for all projects within the selected program."""
if not program:
fig = go.Figure()
fig.update_layout(
title=dict(text="No Program Selected", font=dict(size=20, color="#1f77b4"), x=0.04, xanchor='left'),
xaxis=dict(type='date', tickformat='%b %d %Y', title='Date', tickfont=dict(size=10), showgrid=False, visible=False),
yaxis=dict(title='Tasks', tickfont=dict(size=10), showgrid=False, visible=False),
height=1200,
width=2400,
showlegend=False,
plot_bgcolor='white',
paper_bgcolor='white',
margin=dict(l=150),
annotations=[dict(xref="paper", yref="paper", x=0.5, y=0.5, text="Please select a program to view the timeline.", showarrow=False, font=dict(size=16, color="black"))]
)
return pn.Column(pn.pane.Plotly(fig, sizing_mode='stretch_width'), sizing_mode='stretch_width')
program_df = df_Timeline[df_Timeline['Program'] == program].copy()
if program_df.empty:
fig = go.Figure()
fig.update_layout(
title=dict(text=f"No Data for {program}", font=dict(size=20, color="#1f77b4"), x=0.04, xanchor='left'),
xaxis=dict(type='date', tickformat='%b %d %Y', title='Date', tickfont=dict(size=10), showgrid=False, visible=False),
yaxis=dict(title='Tasks', tickfont=dict(size=10), showgrid=False, visible=False),
height=600,
width=2400,
showlegend=False,
plot_bgcolor='white',
paper_bgcolor='white',
margin=dict(l=150),
annotations=[dict(xref="paper", yref="paper", x=0.5, y=0.5, text=f"No timeline data available for {program}.", showarrow=False, font=dict(size=16, color="black"))]
)
return pn.Column(pn.pane.Plotly(fig, sizing_mode='stretch_width'), sizing_mode='stretch_width')
charts = []
for project in sorted(program_df['Project'].unique()):
project_df = program_df[program_df['Project'] == project]
if not project_df.empty:
fig = create_gantt_chart(project, project_df)
charts.append(pn.pane.Plotly(fig, sizing_mode='stretch_width'))
return pn.Column(*charts, sizing_mode='stretch_width')
#--------------------------------------------------------
# Update the Timeline_tab layout
#--------------------------------------------------------
# formatted_date is define earlier in the code
Timeline_tab = pn.Column(
pn.Row(
pn.pane.HTML(f"""
<div style='background-color:#4472C4; padding:10px'>
<h1 style='color:white; margin:0'>Timeline [{formatted_date}]</h1>
</div>
""", sizing_mode='stretch_width'),
sizing_mode='stretch_width'
),
pn.Row(
program_widget_timeline,
),
pn.layout.Divider(styles={'margin': '10px 0'}),
Timeline_chart,
sizing_mode='stretch_width',
)
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#*****************************************************************************************************************************
##############################################################################################################################
# Define Tabs and serve
##############################################################################################################################
#*****************************************************************************************************************************
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
print('Script successfully completed')
tabs = pn.Tabs(
("Dashboard", cover_dashboard),
("Products Status", cadrans_dashboard),
("Project Progress", historic_tab),
("Clear to Build Summary", summary_tab),
("Priority List", priority_tab),
("Snapshot", Snapshot_tab),
("Timeline - Gantt", Timeline_tab) # 05/19
)
# Inject custom CSS to scale down the dashboard
pn.config.raw_css = ["""
.pn-column {
transform: scale(0.8); /* Scale down to 80% of the original size */
transform-origin: top left; /* Ensure scaling starts from the top-left corner */
width: 125%; /* Compensate for the scaling to avoid empty space */
height: 125%; /* Compensate for the scaling to avoid empty space */
}
"""]
# Inject custom CSS to set the background color to white
pn.config.raw_css = ["""
body, .pn-column, .bk-root {
background-color: white !important;
}
"""]
# Render the Dashboard
tabs.servable()
print('Panel dashboard loaded')