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
3 changes: 3 additions & 0 deletions submissions/Dia-Vats/level6/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your_password_here
4 changes: 4 additions & 0 deletions submissions/Dia-Vats/level6/.streamlit/secrets.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[NEO4J]
NEO4J_URI = "bolt://localhost:7687"
NEO4J_USER = "neo4j"
NEO4J_PASSWORD = "your_password_here"
1 change: 1 addition & 0 deletions submissions/Dia-Vats/level6/DASHBOARD_URL.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://diavats-l6.streamlit.app
108 changes: 108 additions & 0 deletions submissions/Dia-Vats/level6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Steel Factory Production Dashboard
**Dia Vats | Level 6 - LifeAtlas LPI Developer Kit**
**Live: https://diavats-l6.streamlit.app**

---

## What I Built

The raw data was 3 CSVs describing a Swedish steel factory - 8 projects, 9 stations, 14 workers, 8 weeks. I turned it into a Neo4j knowledge graph and built a 7-page Streamlit dashboard on top of it.

The point wasn't to build a chart on top of a spreadsheet. The graph models actual operational dependencies - which work orders flow through which stations, which workers can cover which stations if someone's out, and where downstream risk accumulates when a station overruns. The dashboard surfaces that reasoning directly.

I also completed 2 bonus pages (Bonus B — Factory Floor, Bonus C — Forecast).

**Graph stats: 148 nodes, 446 relationships, 9 labels, 12 relationship types.**

---

## Graph Schema

**Nodes:** Project, WorkOrder, Station, Product, Week, Worker, Certification, CapacitySnapshot, Etapp

**Relationships:** HAS_WORKORDER, AT_STATION, PRODUCES, SCHEDULED_IN, HAS_CAPACITY, ASSIGNED_TO, CAN_COVER, CERTIFIED_IN, REQUIRES, FEEDS_INTO, FOLLOWS, IN_ETAPP

A few things worth noting:

- WorkOrder is the core unit - one node per row in the production CSV. It sits between Project and Station and carries planned hours, actual hours, variance %, and bottleneck flag.
- FEEDS_INTO chains stations in physical flow order (011 → 012 → ... → 021). This lets you trace downstream impact from any overrunning station.
- FOLLOWS links the same project-station pair across consecutive weeks so you can traverse time without aggregating in every query.
- Bottleneck rule: `actual_hours > planned_hours × 1.1`
- WorkOrder ID format: `P01_011_w1_IQB`

---

## Dashboard Pages

**Page 1 - Project Overview**
KPI cards, grouped bar chart (planned vs actual per project), variance table with red highlights where variance exceeds 10%.

**Page 2 - Station Load**
Plotly heatmap, stations vs weeks, coloured green/yellow/red by variance. Station 016 shows up clearly as the recurring problem.

**Page 3 - Capacity Tracker**
Weekly capacity vs planned demand. Deficit weeks in red. Calls out that 5 of 8 weeks run over capacity, worst at -132 hours in week 1.

**Page 4 - Worker Coverage**
Shows who covers which station and flags single points of failure. Station 016 is marked CRITICAL — Victor Elm is the only backup for Per Hansen, and 4 projects run through that station.

**Page 5 - Factory Floor (Bonus B)**
Scatter-based floor plan with stations on a physical grid, coloured by load severity. Hover shows active projects and overload %.

**Page 6 - Forecast (Bonus C)**
Linear extrapolation from weeks 1–8 per station, projecting week 9. Shows which stations are trending toward overload.

**Page 7 - Self-Test**
6 automated Neo4j checks, scored out of 20. Runs on every page load.

---

## Project Structure

```
submissions/Dia-Vats/level6/
├── app.py
├── db.py
├── seed_graph.py
├── requirements.txt
├── .env.example
├── DASHBOARD_URL.txt
├── README.md
├── data/
│ ├── factory_production.csv
│ ├── factory_workers.csv
│ └── factory_capacity.csv
├── pages_impl/
│ ├── page1_overview.py
│ ├── page2_station.py
│ ├── page3_capacity.py
│ ├── page4_workers.py
│ ├── page5_floor.py
│ ├── page6_forecast.py
│ └── page7_selftest.py
└── .streamlit/
└── secrets.toml.example
```

---

## Running It

```bash
pip install -r requirements.txt
python seed_graph.py
streamlit run app.py
```

For Streamlit Cloud, add credentials under Settings > Secrets:
```toml
NEO4J_URI = "neo4j+s://your-instance.databases.neo4j.io"
NEO4J_USER = "your-username"
NEO4J_PASSWORD = "your-password"
```

`seed_graph.py` uses MERGE everywhere — safe to re-run without duplicating data.

---

*Made by Dia Vats*
105 changes: 105 additions & 0 deletions submissions/Dia-Vats/level6/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
app.py — Swedish Steel Factory Dashboard
Author: Dia Vats
7-page Streamlit + Neo4j dashboard.
"""
import streamlit as st

st.set_page_config(
page_title="Steel Factory Dashboard",
page_icon="[SF]",
layout="wide",
initial_sidebar_state="expanded",
)

# ── Global styles ─────────────────────────────────────────────────────────────
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');

html, body, [class*="css"] { font-family: 'Inter', sans-serif; }

h1 { font-family: 'Inter', sans-serif; font-weight: 600; color: #1a1a2e; letter-spacing: -0.5px; }
h2 { font-family: 'Inter', sans-serif; font-weight: 500; color: #2d2d44; }
.section-divider { border: none; border-top: 1px solid #e0e0e0; margin: 1rem 0; }

section[data-testid="stSidebar"] {
background: linear-gradient(180deg,#0f1117 0%,#1a1d2e 100%);
}
section[data-testid="stSidebar"] * { color: #e2e8f0 !important; }

.metric-card {
background: linear-gradient(135deg,#1e2235,#252a3f);
border: 1px solid #2d3454;
border-radius: 12px;
padding: 18px 22px;
margin-bottom: 8px;
}
.metric-card .label { color:#94a3b8; font-size:0.78rem; text-transform:uppercase; letter-spacing:1px; }
.metric-card .value { color:#f1f5f9; font-size:2rem; font-weight:700; }

.spof-badge {
color: #E53935; font-weight: 700; font-size: 0.82rem;
border: 1px solid #E53935; border-radius: 4px;
padding: 1px 7px; letter-spacing: 0.5px;
}
.ok-badge {
color: #43A047; font-weight: 700; font-size: 0.82rem;
border: 1px solid #43A047; border-radius: 4px;
padding: 1px 7px; letter-spacing: 0.5px;
}
.footer-bar {
text-align:center; color:#64748b; font-size:0.75rem;
margin-top:40px; padding-top:16px;
border-top:1px solid #1e2235;
}
.page-title {
border-left: 4px solid #4A90D9;
padding-left: 12px;
margin-bottom: 0.25rem;
font-size: 1.6rem;
font-weight: 600;
color: #1a1a2e;
}
</style>
""", unsafe_allow_html=True)

PAGES = [
"Project Overview",
"Station Load",
"Capacity Tracker",
"Worker Coverage",
"Factory Floor",
"Forecast",
"Self-Test",
]

# ── Sidebar navigation ────────────────────────────────────────────────────────
with st.sidebar:
st.image("https://img.icons8.com/fluency/96/factory.png", width=56)
st.markdown("## Steel Factory")
st.markdown("*Neo4j Production Dashboard*")
st.markdown("---")
page = st.radio("Navigate", PAGES, label_visibility="collapsed")
st.markdown("---")
st.caption("Made by **Dia Vats**")

# ── Footer helper ─────────────────────────────────────────────────────────────
def footer():
st.caption("Made by Dia Vats")

# ── Route to page ─────────────────────────────────────────────────────────────
if page == PAGES[0]:
from pages_impl.page1_overview import render; render(); footer()
elif page == PAGES[1]:
from pages_impl.page2_station import render; render(); footer()
elif page == PAGES[2]:
from pages_impl.page3_capacity import render; render(); footer()
elif page == PAGES[3]:
from pages_impl.page4_workers import render; render(); footer()
elif page == PAGES[4]:
from pages_impl.page5_floor import render; render(); footer()
elif page == PAGES[5]:
from pages_impl.page6_forecast import render; render(); footer()
elif page == PAGES[6]:
from pages_impl.page7_selftest import render; render(); footer()
9 changes: 9 additions & 0 deletions submissions/Dia-Vats/level6/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 submissions/Dia-Vats/level6/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 submissions/Dia-Vats/level6/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
25 changes: 25 additions & 0 deletions submissions/Dia-Vats/level6/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""db.py — shared Neo4j driver for the Streamlit dashboard."""
import os
import streamlit as st
from neo4j import GraphDatabase

@st.cache_resource(show_spinner=False)
def get_driver():
try:
uri = st.secrets["NEO4J_URI"]
user = st.secrets["NEO4J_USER"]
pw = st.secrets["NEO4J_PASSWORD"]
except Exception:
from dotenv import load_dotenv
load_dotenv()
uri = os.getenv("NEO4J_URI", "bolt://localhost:7687")
user = os.getenv("NEO4J_USER", "neo4j")
pw = os.getenv("NEO4J_PASSWORD", "password")
return GraphDatabase.driver(uri, auth=(user, pw))


def run_query(cypher: str, params: dict | None = None) -> list[dict]:
driver = get_driver()
with driver.session() as s:
result = s.run(cypher, params or {})
return [dict(r) for r in result]
1 change: 1 addition & 0 deletions submissions/Dia-Vats/level6/pages_impl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# pages_impl/__init__.py
Loading
Loading