Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions examples/factory_graph_dashboard/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import streamlit as st
from neo4j import GraphDatabase
import pandas as pd
import plotly.express as px

URI = "bolt://localhost:7687"
USER = "neo4j"
PASSWORD = "password123"

driver = GraphDatabase.driver(URI, auth=(USER, PASSWORD))

st.set_page_config(
page_title="Factory Graph Dashboard",
layout="wide"
)

st.title(" Factory Production Knowledge Graph Dashboard")


def run_query(query):
with driver.session() as session:
result = session.run(query)
return [dict(record) for record in result]


# -------------------------------
# KPI SECTION
# -------------------------------

st.header("Factory KPIs")

capacity_query = """
MATCH (c:Capacity)
RETURN
sum(c.total_capacity) as capacity,
sum(c.total_planned) as planned,
sum(c.deficit) as deficit
"""

capacity_data = run_query(capacity_query)[0]

col1, col2, col3 = st.columns(3)

col1.metric("Total Capacity Hours", capacity_data["capacity"])
col2.metric("Total Planned Hours", capacity_data["planned"])
col3.metric("Net Deficit", capacity_data["deficit"])
# -------------------------------
# EXECUTIVE SUMMARY
# -------------------------------

st.header("Executive Summary")

summary_query = """
MATCH (p:Project)-[r:GOES_THROUGH]->(s:Station)
WHERE r.actual_hours > r.planned_hours
RETURN
count(DISTINCT p) AS overloaded_projects,
max(r.actual_hours - r.planned_hours) AS max_overrun
"""

summary = run_query(summary_query)[0]

c1, c2 = st.columns(2)

c1.metric(
"Overloaded Projects",
summary["overloaded_projects"]
)

c2.metric(
"Max Overrun Hours",
summary["max_overrun"]
)

# -------------------------------
# BOTTLENECK ANALYSIS
# -------------------------------

st.header("Station Bottleneck Analysis")

bottleneck_query = """
MATCH (p:Project)-[r:GOES_THROUGH]->(s:Station)
RETURN
s.name as station,
sum(r.planned_hours) as planned,
sum(r.actual_hours) as actual
ORDER BY actual DESC
"""

bottleneck = pd.DataFrame(run_query(bottleneck_query))

fig = px.bar(
bottleneck,
x="station",
y=["planned", "actual"],
barmode="group",
title="Planned vs Actual Hours by Station"
)

st.plotly_chart(fig, use_container_width=True)


# -------------------------------
# WORKER COVERAGE
# -------------------------------

st.header("Worker Coverage")

worker_query = """
MATCH (w:Worker)-[:CAN_COVER]->(s:Station)
RETURN
w.name as worker,
collect(s.code) as stations
"""

workers = run_query(worker_query)

for worker in workers:
st.write(f"**{worker['worker']}** → {', '.join(worker['stations'])}")

# -------------------------------
# WORKER COVERAGE HEATMAP
# -------------------------------

st.header("Worker Coverage Heatmap")

coverage_query = """
MATCH (w:Worker)-[:CAN_COVER]->(s:Station)
RETURN
s.code AS station,
count(w) AS worker_count
ORDER BY station
"""

coverage_df = pd.DataFrame(run_query(coverage_query))

if not coverage_df.empty:

fig = px.bar(
coverage_df,
x="station",
y="worker_count",
title="Certified Worker Coverage by Station"
)

st.plotly_chart(fig, use_container_width=True)

weak = coverage_df[
coverage_df["worker_count"] <= 1
]

if not weak.empty:
st.error("Single Point Failure Stations")
st.dataframe(weak)

# -------------------------------
# PROJECT OVERLOAD
# -------------------------------

st.header("Overloaded Projects")

overload_query = """
MATCH (p:Project)-[r:GOES_THROUGH]->(s:Station)
WHERE r.actual_hours > r.planned_hours * 1.1
RETURN
p.name as project,
s.name as station,
r.planned_hours as planned,
r.actual_hours as actual
"""

overloaded = pd.DataFrame(run_query(overload_query))

st.dataframe(overloaded, use_container_width=True)
if not overloaded.empty:

sample = overloaded.iloc[0]

st.info(
f"""
Why flagged?

Project: {sample['project']}
Station: {sample['station']}

Planned Hours: {sample['planned']}
Actual Hours: {sample['actual']}

Reason:
Actual effort exceeded planned threshold by >10%.
Operational overload detected.
"""
)



# -------------------------------
# RISK ANALYSIS
# -------------------------------

st.header("Operational Risk Analysis")

if not overloaded.empty:

overloaded["variance_percent"] = (
(overloaded["actual"] - overloaded["planned"])
/ overloaded["planned"]
) * 100

def classify_risk(v):
if v >= 30:
return "HIGH"
elif v >= 15:
return "MEDIUM"
return "LOW"

overloaded["risk_level"] = overloaded["variance_percent"].apply(classify_risk)

st.dataframe(
overloaded[
["project", "station", "planned", "actual", "variance_percent", "risk_level"]
],
use_container_width=True
)

high_risk_count = len(
overloaded[overloaded["risk_level"] == "HIGH"]
)

medium_risk_count = len(
overloaded[overloaded["risk_level"] == "MEDIUM"]
)

low_risk_count = len(
overloaded[overloaded["risk_level"] == "LOW"]
)

c1, c2, c3 = st.columns(3)

c1.error(f"HIGH RISK: {high_risk_count}")
c2.warning(f"MEDIUM RISK: {medium_risk_count}")
c3.success(f"LOW RISK: {low_risk_count}")


# -------------------------------
# STATION SEARCH TOOL
# -------------------------------

st.header("Station Search")

station_code = st.text_input("Enter Station Code")

if station_code:
query = f"""
MATCH (w:Worker)-[:CAN_COVER]->(s:Station {{code: '{station_code}'}})
RETURN w.name as worker, w.role as role
"""

results = pd.DataFrame(run_query(query))

if not results.empty:
st.dataframe(results)
else:
st.warning("No workers found")


driver.close()
9 changes: 9 additions & 0 deletions examples/factory_graph_dashboard/data/factory_capacity.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
week,own_staff_count,hired_staff_count,own_hours,hired_hours,overtime_hours,total_capacity,total_planned,deficit
w1,10,2,400,80,0,480,612,-132
w2,10,2,400,80,40,520,645,-125
w3,10,2,400,80,0,480,398,82
w4,10,2,400,80,20,500,550,-50
w5,10,2,400,80,30,510,480,30
w6,9,2,360,80,0,440,520,-80
w7,10,2,400,80,40,520,600,-80
w8,10,2,400,80,20,500,470,30
69 changes: 69 additions & 0 deletions examples/factory_graph_dashboard/data/factory_production.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
project_id,project_number,project_name,product_type,unit,quantity,unit_factor,station_code,station_name,etapp,bop,week,planned_hours,actual_hours,completed_units
P01,4501,Stålverket Borås,IQB,meter,600,1.77,011,FS IQB,ET1,BOP1,w1,48.0,45.2,28
P01,4501,Stålverket Borås,IQB,meter,600,1.77,012,Förmontering IQB,ET1,BOP1,w1,32.0,35.5,25
P01,4501,Stålverket Borås,IQB,meter,600,1.77,013,Montering IQB,ET1,BOP1,w1,28.0,26.0,22
P01,4501,Stålverket Borås,IQB,meter,600,1.77,014,Svets o montage IQB,ET1,BOP1,w1,35.0,38.2,20
P01,4501,Stålverket Borås,SB,styck,40,4.0,018,SB B/F-hall,ET1,BOP1,w1,16.0,14.5,4
P01,4501,Stålverket Borås,SP,styck,180,2.0,019,SP B/F-hall,ET1,BOP1,w1,12.0,13.0,7
P01,4501,Stålverket Borås,IQB,meter,600,1.77,011,FS IQB,ET1,BOP1,w2,48.0,50.0,32
P01,4501,Stålverket Borås,IQB,meter,600,1.77,012,Förmontering IQB,ET1,BOP1,w2,32.0,30.0,28
P01,4501,Stålverket Borås,IQP,styck,90,2.80,015,Montering IQP,ET1,BOP2,w2,25.0,28.0,9
P01,4501,Stålverket Borås,SR,styck,8,45.0,021,SR B/F-hall,ET1,BOP2,w2,40.0,42.0,1
P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,011,FS IQB,ET1,BOP1,w1,30.0,28.0,20
P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,012,Förmontering IQB,ET1,BOP1,w1,22.0,24.5,18
P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,013,Montering IQB,ET1,BOP1,w1,18.0,17.0,16
P02,4502,Kontorshus Mölndal,IQP,styck,70,2.70,015,Montering IQP,ET1,BOP1,w1,19.0,21.0,7
P02,4502,Kontorshus Mölndal,SD,styck,30,3.00,018,SB B/F-hall,ET1,BOP1,w1,9.0,8.5,3
P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,011,FS IQB,ET1,BOP1,w2,30.0,32.0,24
P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,014,Svets o montage IQB,ET1,BOP1,w2,25.0,23.0,20
P02,4502,Kontorshus Mölndal,SP,styck,120,1.75,019,SP B/F-hall,ET1,BOP2,w2,14.0,15.5,8
P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,011,FS IQB,ET1,BOP1,w1,72.0,70.0,40
P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,012,Förmontering IQB,ET1,BOP1,w1,48.0,52.0,35
P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,013,Montering IQB,ET1,BOP1,w1,38.0,36.5,30
P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,014,Svets o montage IQB,ET1,BOP1,w1,42.0,48.0,28
P03,4503,Lagerhall Jönköping,SB,styck,60,6.00,018,SB B/F-hall,ET1,BOP1,w1,36.0,38.0,6
P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,011,FS IQB,ET1,BOP1,w2,72.0,75.0,45
P03,4503,Lagerhall Jönköping,IQP,styck,110,2.90,015,Montering IQP,ET1,BOP2,w2,32.0,30.0,11
P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,016,Gjutning,ET1,BOP2,w2,28.0,35.0,8
P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,017,Målning,ET1,BOP2,w3,24.0,22.0,20
P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,011,FS IQB,ET1,BOP1,w1,38.0,36.0,24
P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,012,Förmontering IQB,ET1,BOP1,w1,25.0,27.0,20
P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,013,Montering IQB,ET1,BOP1,w1,20.0,19.0,18
P04,4504,Parkering Helsingborg,IQP,styck,55,2.85,015,Montering IQP,ET1,BOP1,w1,16.0,18.0,6
P04,4504,Parkering Helsingborg,SB,styck,25,7.50,018,SB B/F-hall,ET1,BOP1,w1,19.0,22.0,3
P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,011,FS IQB,ET1,BOP1,w2,38.0,40.0,28
P04,4504,Parkering Helsingborg,SP,styck,100,2.00,019,SP B/F-hall,ET1,BOP2,w2,12.0,11.0,6
P04,4504,Parkering Helsingborg,SR,styck,12,120.0,021,SR B/F-hall,ET1,BOP2,w2,60.0,65.0,1
P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,011,FS IQB,ET2,BOP3,w1,95.0,90.0,50
P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,012,Förmontering IQB,ET2,BOP3,w1,65.0,68.0,42
P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,013,Montering IQB,ET2,BOP3,w1,50.0,48.0,38
P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,014,Svets o montage IQB,ET2,BOP3,w1,58.0,62.0,35
P05,4505,Sjukhus Linköping ET2,IQP,styck,150,2.88,015,Montering IQP,ET2,BOP3,w1,30.0,33.0,10
P05,4505,Sjukhus Linköping ET2,SB,styck,50,5.00,018,SB B/F-hall,ET2,BOP3,w1,25.0,28.0,5
P05,4505,Sjukhus Linköping ET2,SD,styck,45,2.75,018,SB B/F-hall,ET2,BOP3,w1,12.0,11.5,4
P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,011,FS IQB,ET2,BOP3,w2,95.0,98.0,55
P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,016,Gjutning,ET2,BOP3,w2,35.0,40.0,12
P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,017,Målning,ET2,BOP3,w2,28.0,26.0,25
P05,4505,Sjukhus Linköping ET2,SR,styck,20,274.0,021,SR B/F-hall,ET2,BOP3,w3,120.0,115.0,2
P06,4506,Skola Uppsala,IQB,meter,500,1.60,011,FS IQB,ET1,BOP1,w2,40.0,38.0,26
P06,4506,Skola Uppsala,IQB,meter,500,1.60,012,Förmontering IQB,ET1,BOP1,w2,28.0,30.0,22
P06,4506,Skola Uppsala,IQB,meter,500,1.60,013,Montering IQB,ET1,BOP1,w2,22.0,20.0,18
P06,4506,Skola Uppsala,IQP,styck,80,2.75,015,Montering IQP,ET1,BOP1,w2,22.0,24.0,8
P06,4506,Skola Uppsala,SB,styck,35,4.50,018,SB B/F-hall,ET1,BOP1,w2,16.0,18.0,4
P06,4506,Skola Uppsala,SP,styck,140,1.50,019,SP B/F-hall,ET1,BOP2,w3,14.0,12.0,10
P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,011,FS IQB,ET1,BOP1,w1,45.0,42.0,22
P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,012,Förmontering IQB,ET1,BOP1,w1,30.0,33.0,18
P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,014,Svets o montage IQB,ET1,BOP1,w1,35.0,32.0,16
P07,4507,Idrottshall Västerås,SB,styck,45,3.50,018,SB B/F-hall,ET1,BOP1,w1,16.0,18.0,5
P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,011,FS IQB,ET1,BOP1,w2,45.0,48.0,26
P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,016,Gjutning,ET1,BOP2,w2,20.0,22.0,5
P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,017,Målning,ET1,BOP2,w3,18.0,16.0,15
P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,011,FS IQB,ET1,BOP1,w1,65.0,62.0,36
P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,012,Förmontering IQB,ET1,BOP1,w1,42.0,45.0,30
P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,013,Montering IQB,ET1,BOP1,w1,35.0,38.0,25
P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,014,Svets o montage IQB,ET1,BOP1,w1,40.0,44.0,22
P08,4508,Bro E6 Halmstad,SP,styck,200,2.50,019,SP B/F-hall,ET1,BOP1,w1,20.0,18.0,8
P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,011,FS IQB,ET1,BOP1,w2,65.0,68.0,42
P08,4508,Bro E6 Halmstad,IQP,styck,95,2.93,015,Montering IQP,ET1,BOP2,w2,28.0,30.0,10
P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,016,Gjutning,ET1,BOP2,w3,22.0,25.0,8
P08,4508,Bro E6 Halmstad,SR,styck,15,180.0,021,SR B/F-hall,ET1,BOP2,w3,90.0,85.0,2
15 changes: 15 additions & 0 deletions examples/factory_graph_dashboard/data/factory_workers.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
worker_id,name,role,primary_station,can_cover_stations,certifications,hours_per_week,type
W01,Erik Lindberg,Operator,011,"011,012","MIG/MAG,TIG,ISO 9606",40,permanent
W02,Anna Berg,Operator,011,"011,014","MIG/MAG,TIG",40,permanent
W03,Lars Jensen,Operator,012,"012,013","Surface treatment,CE marking",40,permanent
W04,Maria Stone,Operator,013,"013","Blasting,Surface protection",40,permanent
W05,Johan Peters,Operator,014,"014,015","Hydraulics,Mechanics,Crane",40,permanent
W06,Karen Nilsen,Inspector,015,"015","SIS,SS-EN 1090,NDT",40,permanent
W07,Per Hansen,Operator,016,"016,017","Casting,Formwork",40,permanent
W08,Sofia Arden,Operator,017,"017","Surface treatment,Spray painting",40,permanent
W09,Magnus Stone,Operator,018,"018,019","Sheet metal,Assembly",40,permanent
W10,Elin Frank,Operator,019,"019,018","Assembly,Welding",32,permanent
W11,Victor Elm,Foreman,all,"011,012,013,014,015,016,017,018,019,021","Leadership,CE,ISO 9001",45,permanent
W12,Lena Dale,Quality Manager,015,"015","ISO 9001,SS-EN 1090,Audit",40,permanent
W13,Ahmed Hassan,Operator,011,"011","MIG/MAG",40,hired
W14,Petra Steen,Operator,012,"012,013","Surface treatment",40,hired
Empty file.
Loading
Loading