| | """ |
| | Kit Relationship Visualization |
| | Shows the actual dependency relationships between kits in production |
| | based on kit_hierarchy.json data |
| | """ |
| |
|
| | import streamlit as st |
| | import pandas as pd |
| | import plotly.express as px |
| | import plotly.graph_objects as go |
| | from plotly.subplots import make_subplots |
| | import json |
| | import sys |
| | sys.path.append('src') |
| |
|
| | from config.constants import ShiftType, LineType, KitLevel |
| |
|
| | |
| | try: |
| | import networkx as nx |
| | NETWORKX_AVAILABLE = True |
| | except ImportError: |
| | NETWORKX_AVAILABLE = False |
| | nx = None |
| |
|
| | def load_kit_hierarchy(): |
| | """Load kit hierarchy data from JSON file""" |
| | try: |
| | with open('data/hierarchy_exports/kit_hierarchy.json', 'r') as f: |
| | return json.load(f) |
| | except FileNotFoundError: |
| | st.error("Kit hierarchy file not found. Please ensure kit_hierarchy.json exists in data/hierarchy_exports/") |
| | return {} |
| | except json.JSONDecodeError: |
| | st.error("Invalid kit hierarchy JSON format") |
| | return {} |
| |
|
| | def display_kit_relationships_dashboard(results): |
| | """Main dashboard showing kit relationships in production""" |
| | st.header("π Kit Relationship Dashboard") |
| | st.markdown("Visualizing dependencies between kits being produced") |
| | st.markdown("---") |
| | |
| | |
| | hierarchy_data = load_kit_hierarchy() |
| | |
| | if not hierarchy_data: |
| | st.warning("No kit hierarchy data available") |
| | return |
| | |
| | |
| | produced_kits = set() |
| | if 'weekly_production' in results: |
| | produced_kits = set(results['weekly_production'].keys()) |
| | elif 'run_schedule' in results: |
| | produced_kits = set(row['product'] for row in results['run_schedule']) |
| | |
| | if not produced_kits: |
| | st.warning("No production data available") |
| | return |
| | |
| | |
| | tab1, tab2, tab3, tab4 = st.tabs([ |
| | "π Dependency Network", |
| | "π Relationship Matrix", |
| | "π― Production Flow", |
| | "β οΈ Dependency Analysis" |
| | ]) |
| | |
| | with tab1: |
| | display_dependency_network(hierarchy_data, produced_kits, results) |
| | |
| | with tab2: |
| | display_relationship_matrix(hierarchy_data, produced_kits, results) |
| | |
| | with tab3: |
| | display_production_flow_relationships(hierarchy_data, produced_kits, results) |
| | |
| | with tab4: |
| | display_dependency_analysis(hierarchy_data, produced_kits, results) |
| |
|
| | def display_dependency_network(hierarchy_data, produced_kits, results): |
| | """Show interactive network graph of kit dependencies""" |
| | st.subheader("π Kit Dependency Network") |
| | st.markdown("Interactive graph showing which kits depend on other kits") |
| | |
| | |
| | relationships = build_relationship_data(hierarchy_data, produced_kits) |
| | |
| | if not relationships: |
| | st.info("No dependency relationships found between produced kits") |
| | return |
| | |
| | |
| | production_timing = get_production_timing(results) |
| | |
| | |
| | col1, col2 = st.columns([3, 1]) |
| | |
| | with col1: |
| | if NETWORKX_AVAILABLE: |
| | fig = create_interactive_network_graph(relationships, production_timing) |
| | st.plotly_chart(fig, use_container_width=True) |
| | else: |
| | fig = create_simple_dependency_chart(relationships, production_timing) |
| | st.plotly_chart(fig, use_container_width=True) |
| | st.info("π‘ Install networkx for advanced network layouts: `pip install networkx`") |
| | |
| | with col2: |
| | |
| | st.subheader("π Network Stats") |
| | |
| | all_kits = set() |
| | for rel in relationships: |
| | all_kits.add(rel['source']) |
| | all_kits.add(rel['target']) |
| | |
| | st.metric("Total Kits", len(all_kits)) |
| | st.metric("Dependencies", len(relationships)) |
| | |
| | |
| | max_depth = calculate_dependency_depth(relationships) |
| | st.metric("Max Dependency Depth", max_depth) |
| | |
| | |
| | dependent_kits = get_most_dependent_kits(relationships) |
| | st.subheader("π Most Dependencies") |
| | for kit, count in dependent_kits[:5]: |
| | st.write(f"**{kit}**: {count} dependencies") |
| |
|
| | def display_relationship_matrix(hierarchy_data, produced_kits, results): |
| | """Show dependency matrix heatmap""" |
| | st.subheader("π Kit Dependency Matrix") |
| | st.markdown("Heatmap showing which kits (rows) depend on which other kits (columns)") |
| | |
| | |
| | matrix_data = build_dependency_matrix(hierarchy_data, produced_kits) |
| | |
| | if matrix_data.empty: |
| | st.info("No dependency relationships to visualize in matrix form") |
| | return |
| | |
| | |
| | fig = px.imshow(matrix_data.values, |
| | x=matrix_data.columns, |
| | y=matrix_data.index, |
| | color_continuous_scale='Blues', |
| | title='Kit Dependency Matrix (1 = depends on, 0 = no dependency)', |
| | labels=dict(x="Dependency (what is needed)", |
| | y="Kit (what depends on others)", |
| | color="Dependency")) |
| | |
| | fig.update_layout(height=600) |
| | st.plotly_chart(fig, use_container_width=True) |
| | |
| | |
| | with st.expander("π View Dependency Matrix as Table"): |
| | st.dataframe(matrix_data, use_container_width=True) |
| |
|
| | def display_production_flow_relationships(hierarchy_data, produced_kits, results): |
| | """Show how relationships affect production timing""" |
| | st.subheader("π― Production Flow with Relationships") |
| | st.markdown("Timeline showing when dependent kits are produced") |
| | |
| | |
| | production_timing = get_production_timing(results) |
| | relationships = build_relationship_data(hierarchy_data, produced_kits) |
| | |
| | if not production_timing or not relationships: |
| | st.info("Insufficient data for production flow analysis") |
| | return |
| | |
| | |
| | fig = create_production_timeline_with_dependencies(production_timing, relationships) |
| | st.plotly_chart(fig, use_container_width=True) |
| | |
| | |
| | st.subheader("β° Dependency Timing Analysis") |
| | timing_analysis = analyze_dependency_timing(production_timing, relationships) |
| | |
| | if timing_analysis: |
| | df = pd.DataFrame(timing_analysis) |
| | st.dataframe(df, use_container_width=True) |
| |
|
| | def display_dependency_analysis(hierarchy_data, produced_kits, results): |
| | """Analyze dependency fulfillment and violations""" |
| | st.subheader("β οΈ Dependency Analysis & Violations") |
| | |
| | production_timing = get_production_timing(results) |
| | relationships = build_relationship_data(hierarchy_data, produced_kits) |
| | |
| | |
| | violations = find_dependency_violations(production_timing, relationships) |
| | |
| | |
| | col1, col2, col3, col4 = st.columns(4) |
| | |
| | with col1: |
| | total_deps = len(relationships) |
| | st.metric("Total Dependencies", total_deps) |
| | |
| | with col2: |
| | violated_deps = len(violations) |
| | st.metric("Violations", violated_deps, |
| | delta=f"-{violated_deps}" if violated_deps > 0 else None) |
| | |
| | with col3: |
| | if total_deps > 0: |
| | success_rate = ((total_deps - violated_deps) / total_deps) * 100 |
| | st.metric("Success Rate", f"{success_rate:.1f}%") |
| | else: |
| | st.metric("Success Rate", "N/A") |
| | |
| | with col4: |
| | if violations: |
| | avg_violation = sum(v['days_early'] for v in violations) / len(violations) |
| | st.metric("Avg Days Early", f"{avg_violation:.1f}") |
| | else: |
| | st.metric("Avg Days Early", "0") |
| | |
| | |
| | if violations: |
| | st.subheader("π¨ Dependency Violations") |
| | st.markdown("Cases where kits were produced before their dependencies") |
| | |
| | violation_df = pd.DataFrame(violations) |
| | |
| | |
| | fig = px.scatter(violation_df, |
| | x='dependency_day', y='kit_day', |
| | size='days_early', color='severity', |
| | hover_data=['kit', 'dependency'], |
| | title='Dependency Violations (Below diagonal = violation)', |
| | labels={'dependency_day': 'When Dependency Was Made', |
| | 'kit_day': 'When Kit Was Made'}) |
| | |
| | |
| | max_day = max(violation_df['dependency_day'].max(), violation_df['kit_day'].max()) |
| | fig.add_shape(type="line", x0=0, y0=0, x1=max_day, y1=max_day, |
| | line=dict(dash="dash", color="green"), |
| | name="Ideal Timeline") |
| | |
| | st.plotly_chart(fig, use_container_width=True) |
| | |
| | |
| | st.dataframe(violation_df[['kit', 'dependency', 'kit_day', 'dependency_day', |
| | 'days_early', 'severity']], use_container_width=True) |
| | else: |
| | st.success("π No dependency violations found! All kits produced in correct order.") |
| | |
| | |
| | st.subheader("π‘ Recommendations") |
| | recommendations = generate_dependency_recommendations(violations, relationships, production_timing) |
| | for rec in recommendations: |
| | st.info(f"π‘ {rec}") |
| |
|
| | |
| |
|
| | def build_relationship_data(hierarchy_data, produced_kits): |
| | """Build relationship data for visualization""" |
| | relationships = [] |
| | |
| | for kit_id, kit_info in hierarchy_data.items(): |
| | if kit_id not in produced_kits: |
| | continue |
| | |
| | |
| | dependencies = kit_info.get('dependencies', []) |
| | for dep in dependencies: |
| | if dep in produced_kits: |
| | relationships.append({ |
| | 'source': dep, |
| | 'target': kit_id, |
| | 'type': 'direct', |
| | 'source_type': hierarchy_data.get(dep, {}).get('type', 'unknown'), |
| | 'target_type': kit_info.get('type', 'unknown') |
| | }) |
| | |
| | return relationships |
| |
|
| | def build_dependency_matrix(hierarchy_data, produced_kits): |
| | """Build dependency matrix for heatmap""" |
| | produced_list = sorted(list(produced_kits)) |
| | |
| | if len(produced_list) == 0: |
| | return pd.DataFrame() |
| | |
| | |
| | matrix = pd.DataFrame(0, index=produced_list, columns=produced_list) |
| | |
| | |
| | for kit_id in produced_list: |
| | kit_info = hierarchy_data.get(kit_id, {}) |
| | dependencies = kit_info.get('dependencies', []) |
| | |
| | for dep in dependencies: |
| | if dep in produced_list: |
| | matrix.loc[kit_id, dep] = 1 |
| | |
| | return matrix |
| |
|
| | def get_production_timing(results): |
| | """Extract production timing for each kit""" |
| | timing = {} |
| | |
| | if 'run_schedule' in results: |
| | for run in results['run_schedule']: |
| | kit = run['product'] |
| | day = run['day'] |
| | |
| | |
| | if kit not in timing or day < timing[kit]: |
| | timing[kit] = day |
| | |
| | return timing |
| |
|
| | def create_interactive_network_graph(relationships, production_timing): |
| | """Create interactive network graph using NetworkX layout""" |
| | if not NETWORKX_AVAILABLE: |
| | return create_simple_dependency_chart(relationships, production_timing) |
| | |
| | |
| | G = nx.DiGraph() |
| | |
| | |
| | for rel in relationships: |
| | G.add_edge(rel['source'], rel['target'], type=rel['type']) |
| | |
| | if len(G.nodes()) == 0: |
| | return go.Figure().add_annotation( |
| | text="No relationships to display", |
| | xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False |
| | ) |
| | |
| | |
| | pos = nx.spring_layout(G, k=3, iterations=50) |
| | |
| | |
| | edge_x, edge_y = [], [] |
| | edge_info = [] |
| | |
| | for edge in G.edges(): |
| | source, target = edge |
| | x0, y0 = pos[source] |
| | x1, y1 = pos[target] |
| | |
| | edge_x.extend([x0, x1, None]) |
| | edge_y.extend([y0, y1, None]) |
| | |
| | |
| | edge_info.append({ |
| | 'x': (x0 + x1) / 2, |
| | 'y': (y0 + y1) / 2, |
| | 'text': 'β', |
| | 'source': source, |
| | 'target': target |
| | }) |
| | |
| | edge_trace = go.Scatter(x=edge_x, y=edge_y, |
| | line=dict(width=2, color='#888'), |
| | hoverinfo='none', |
| | mode='lines') |
| | |
| | |
| | node_x, node_y, node_text, node_color, node_size = [], [], [], [], [] |
| | node_info = [] |
| | |
| | for node in G.nodes(): |
| | x, y = pos[node] |
| | node_x.append(x) |
| | node_y.append(y) |
| | |
| | |
| | in_degree = G.in_degree(node) |
| | out_degree = G.out_degree(node) |
| | total_degree = in_degree + out_degree |
| | node_size.append(20 + total_degree * 5) |
| | |
| | |
| | prod_day = production_timing.get(node, 0) |
| | if prod_day == 1: |
| | node_color.append('#90EE90') |
| | elif prod_day <= 3: |
| | node_color.append('#FFD700') |
| | else: |
| | node_color.append('#FF6347') |
| | |
| | |
| | short_name = node[:12] + "..." if len(node) > 12 else node |
| | node_text.append(short_name) |
| | |
| | node_info.append(f"{node}<br>Day: {prod_day}<br>In: {in_degree}, Out: {out_degree}") |
| | |
| | node_trace = go.Scatter(x=node_x, y=node_y, |
| | mode='markers+text', |
| | text=node_text, |
| | textposition='middle center', |
| | hovertext=node_info, |
| | hoverinfo='text', |
| | marker=dict(size=node_size, |
| | color=node_color, |
| | line=dict(width=2, color='black'))) |
| | |
| | |
| | fig = go.Figure(data=[edge_trace, node_trace], |
| | layout=go.Layout( |
| | title='Kit Dependency Network (Size=Connections, Color=Production Day)', |
| | showlegend=False, |
| | hovermode='closest', |
| | margin=dict(b=20,l=5,r=5,t=40), |
| | annotations=[ |
| | dict(text="Green=Early, Gold=Middle, Red=Late production", |
| | showarrow=False, |
| | xref="paper", yref="paper", |
| | x=0.005, y=-0.002, |
| | xanchor='left', yanchor='bottom', |
| | font=dict(size=12)) |
| | ], |
| | xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), |
| | yaxis=dict(showgrid=False, zeroline=False, showticklabels=False))) |
| | |
| | return fig |
| |
|
| | def create_simple_dependency_chart(relationships, production_timing): |
| | """Create simple dependency chart without NetworkX""" |
| | if not relationships: |
| | return go.Figure().add_annotation( |
| | text="No dependencies to display", |
| | xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False |
| | ) |
| | |
| | |
| | |
| | sources = set(rel['source'] for rel in relationships) |
| | targets = set(rel['target'] for rel in relationships) |
| | |
| | |
| | all_kits = list(sources | targets) |
| | positions = {kit: (i, production_timing.get(kit, 0)) for i, kit in enumerate(all_kits)} |
| | |
| | |
| | edge_x, edge_y = [], [] |
| | for rel in relationships: |
| | source_pos = positions[rel['source']] |
| | target_pos = positions[rel['target']] |
| | |
| | edge_x.extend([source_pos[0], target_pos[0], None]) |
| | edge_y.extend([source_pos[1], target_pos[1], None]) |
| | |
| | |
| | edge_trace = go.Scatter(x=edge_x, y=edge_y, |
| | line=dict(width=2, color='#888'), |
| | hoverinfo='none', |
| | mode='lines') |
| | |
| | |
| | node_x = [positions[kit][0] for kit in all_kits] |
| | node_y = [positions[kit][1] for kit in all_kits] |
| | node_text = [kit[:10] + "..." if len(kit) > 10 else kit for kit in all_kits] |
| | |
| | node_trace = go.Scatter(x=node_x, y=node_y, |
| | mode='markers+text', |
| | text=node_text, |
| | textposition='top center', |
| | marker=dict(size=15, color='lightblue', |
| | line=dict(width=2, color='black')), |
| | hovertext=all_kits, |
| | hoverinfo='text') |
| | |
| | fig = go.Figure(data=[edge_trace, node_trace], |
| | layout=go.Layout( |
| | title='Kit Dependencies (Y-axis = Production Day)', |
| | showlegend=False, |
| | xaxis=dict(title='Kits'), |
| | yaxis=dict(title='Production Day'))) |
| | |
| | return fig |
| |
|
| | def create_production_timeline_with_dependencies(production_timing, relationships): |
| | """Create timeline showing production order with dependency arrows""" |
| | if not production_timing: |
| | return go.Figure() |
| | |
| | |
| | timeline_data = [] |
| | for kit, day in production_timing.items(): |
| | timeline_data.append({ |
| | 'Kit': kit, |
| | 'Day': day, |
| | 'Short_Name': kit[:15] + "..." if len(kit) > 15 else kit |
| | }) |
| | |
| | df = pd.DataFrame(timeline_data) |
| | |
| | |
| | fig = px.scatter(df, x='Day', y='Kit', |
| | hover_data=['Kit'], |
| | title='Production Timeline with Dependencies') |
| | |
| | |
| | for rel in relationships: |
| | source_day = production_timing.get(rel['source'], 0) |
| | target_day = production_timing.get(rel['target'], 0) |
| | |
| | |
| | if source_day > 0 and target_day > 0: |
| | fig.add_annotation( |
| | x=target_day, y=rel['target'], |
| | ax=source_day, ay=rel['source'], |
| | arrowhead=2, arrowsize=1, arrowwidth=2, |
| | arrowcolor="red" if source_day > target_day else "green" |
| | ) |
| | |
| | fig.update_layout(height=max(400, len(df) * 20)) |
| | return fig |
| |
|
| | def calculate_dependency_depth(relationships): |
| | """Calculate maximum dependency depth""" |
| | if not NETWORKX_AVAILABLE or not relationships: |
| | return 0 |
| | |
| | G = nx.DiGraph() |
| | for rel in relationships: |
| | G.add_edge(rel['source'], rel['target']) |
| | |
| | try: |
| | return nx.dag_longest_path_length(G) |
| | except: |
| | return 0 |
| |
|
| | def get_most_dependent_kits(relationships): |
| | """Get kits with most dependencies""" |
| | dependency_counts = {} |
| | |
| | for rel in relationships: |
| | target = rel['target'] |
| | dependency_counts[target] = dependency_counts.get(target, 0) + 1 |
| | |
| | return sorted(dependency_counts.items(), key=lambda x: x[1], reverse=True) |
| |
|
| | def find_dependency_violations(production_timing, relationships): |
| | """Find cases where kits were produced before their dependencies""" |
| | violations = [] |
| | |
| | for rel in relationships: |
| | source = rel['source'] |
| | target = rel['target'] |
| | |
| | source_day = production_timing.get(source, 0) |
| | target_day = production_timing.get(target, 0) |
| | |
| | if source_day > 0 and target_day > 0 and source_day > target_day: |
| | days_early = source_day - target_day |
| | severity = 'high' if days_early > 2 else 'medium' if days_early > 1 else 'low' |
| | |
| | violations.append({ |
| | 'kit': target, |
| | 'dependency': source, |
| | 'kit_day': target_day, |
| | 'dependency_day': source_day, |
| | 'days_early': days_early, |
| | 'severity': severity |
| | }) |
| | |
| | return violations |
| |
|
| | def analyze_dependency_timing(production_timing, relationships): |
| | """Analyze timing of all dependency relationships""" |
| | timing_analysis = [] |
| | |
| | for rel in relationships: |
| | source = rel['source'] |
| | target = rel['target'] |
| | |
| | source_day = production_timing.get(source, 0) |
| | target_day = production_timing.get(target, 0) |
| | |
| | if source_day > 0 and target_day > 0: |
| | timing_diff = target_day - source_day |
| | status = "β
Correct" if timing_diff >= 0 else "β Violation" |
| | |
| | timing_analysis.append({ |
| | 'Kit': target[:20] + "..." if len(target) > 20 else target, |
| | 'Dependency': source[:20] + "..." if len(source) > 20 else source, |
| | 'Kit Day': target_day, |
| | 'Dep Day': source_day, |
| | 'Gap (Days)': timing_diff, |
| | 'Status': status |
| | }) |
| | |
| | return sorted(timing_analysis, key=lambda x: x['Gap (Days)']) |
| |
|
| | def generate_dependency_recommendations(violations, relationships, production_timing): |
| | """Generate recommendations based on dependency analysis""" |
| | recommendations = [] |
| | |
| | if not violations: |
| | recommendations.append("Excellent! All dependencies are being fulfilled in the correct order.") |
| | return recommendations |
| | |
| | |
| | high_severity = [v for v in violations if v['severity'] == 'high'] |
| | medium_severity = [v for v in violations if v['severity'] == 'medium'] |
| | |
| | if high_severity: |
| | recommendations.append( |
| | f"π¨ High Priority: {len(high_severity)} critical dependency violations found. " |
| | "Consider rescheduling production to ensure dependencies are produced first." |
| | ) |
| | |
| | if medium_severity: |
| | recommendations.append( |
| | f"β οΈ Medium Priority: {len(medium_severity)} moderate dependency timing issues. " |
| | "Review production sequence for optimization opportunities." |
| | ) |
| | |
| | |
| | problem_kits = {} |
| | for v in violations: |
| | kit = v['kit'] |
| | problem_kits[kit] = problem_kits.get(kit, 0) + 1 |
| | |
| | if problem_kits: |
| | worst_kit = max(problem_kits.items(), key=lambda x: x[1]) |
| | recommendations.append( |
| | f"π― Focus Area: Kit {worst_kit[0]} has {worst_kit[1]} dependency issues. " |
| | "Consider moving its production later in the schedule." |
| | ) |
| | |
| | return recommendations |