Spaces:
Runtime error
Runtime error
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +183 -198
src/streamlit_app.py
CHANGED
|
@@ -4,115 +4,119 @@ import numpy as np
|
|
| 4 |
from sklearn.preprocessing import StandardScaler
|
| 5 |
from sklearn.neighbors import KNeighborsRegressor
|
| 6 |
|
| 7 |
-
# ---------------------------
|
| 8 |
-
# 1.
|
| 9 |
-
# ---------------------------
|
| 10 |
-
st.set_page_config(
|
| 11 |
-
page_title="DynoPro for Mac",
|
| 12 |
-
page_icon="π₯οΈ",
|
| 13 |
-
layout="wide",
|
| 14 |
-
initial_sidebar_state="expanded"
|
| 15 |
-
)
|
| 16 |
|
| 17 |
-
# Custom CSS for the Apple/MacOS Aesthetic
|
| 18 |
st.markdown("""
|
| 19 |
-
|
| 20 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
.stApp {
|
| 22 |
-
background-color: #
|
| 23 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
/* Sidebar
|
| 27 |
section[data-testid="stSidebar"] {
|
| 28 |
-
background-color: #
|
| 29 |
-
border-right: 1px solid #
|
| 30 |
}
|
| 31 |
|
| 32 |
-
/*
|
| 33 |
-
|
| 34 |
-
color: #
|
| 35 |
-
font-weight: 600;
|
| 36 |
}
|
| 37 |
|
| 38 |
-
/*
|
| 39 |
-
div[data-testid="
|
| 40 |
-
background-color:
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
border-radius: 12px;
|
| 43 |
-
|
| 44 |
-
border: 1px solid #EAEAEA;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
/*
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
-
/*
|
| 57 |
-
.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
border: none;
|
| 62 |
}
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
# ---------------------------
|
| 67 |
-
# 2. Advanced Dataset Generation
|
| 68 |
-
# ---------------------------
|
| 69 |
@st.cache_resource
|
| 70 |
-
def
|
| 71 |
np.random.seed(42)
|
|
|
|
| 72 |
data = []
|
| 73 |
|
| 74 |
for _ in range(n):
|
| 75 |
-
|
| 76 |
-
cyl = np.random.choice([
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
induction = np.random.choice([0, 1, 2, 3]) # None/Turbo/Twin-Turbo/Super
|
| 93 |
-
intercooler = np.random.choice([0, 1]) # Stock/Upgraded
|
| 94 |
-
fuel = np.random.choice([0, 1, 2, 3]) # 87/91/93/E85
|
| 95 |
-
tune = np.random.choice([0, 1, 2, 3]) # None/Stage1/Stage2/Custom
|
| 96 |
-
|
| 97 |
-
# Logic: V12s gain more from exhaust/headers, Turbos gain huge from intercoolers
|
| 98 |
-
hp_gain = (
|
| 99 |
-
(intake * 4) +
|
| 100 |
-
(headers * (cyl * 1.5)) +
|
| 101 |
-
(exhaust * 8) +
|
| 102 |
-
(induction * (base_hp * 0.30)) + # % gain based on base HP
|
| 103 |
-
(intercooler * (15 if induction > 0 else 0)) +
|
| 104 |
-
(tune * 15) +
|
| 105 |
-
(fuel * 5) +
|
| 106 |
np.random.uniform(-5, 5)
|
| 107 |
)
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
hp_gain = hp_gain * 0.7
|
| 112 |
|
| 113 |
-
data.append([engine, cyl, base_hp, intake,
|
| 114 |
|
| 115 |
-
columns = ["engine", "cyl", "base_hp", "intake", "
|
| 116 |
df = pd.DataFrame(data, columns=columns)
|
| 117 |
|
| 118 |
X = df.drop("hp_gain", axis=1)
|
|
@@ -126,140 +130,121 @@ def build_engine_model(n=800):
|
|
| 126 |
|
| 127 |
return scaler, model
|
| 128 |
|
| 129 |
-
scaler, model =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
# ---------------------------
|
| 132 |
-
# 3. Sidebar (The "Control Center")
|
| 133 |
-
# ---------------------------
|
| 134 |
with st.sidebar:
|
| 135 |
-
st.
|
| 136 |
-
st.caption("Vehicle Configuration")
|
| 137 |
-
|
| 138 |
-
st.subheader("Base Vehicle")
|
| 139 |
-
cyl = st.select_slider("Cylinders", options=[3, 4, 5, 6, 8, 10, 12], value=6)
|
| 140 |
-
engine = st.number_input("Displacement (L)", 1.0, 8.4, 3.0, step=0.1)
|
| 141 |
-
base_hp = st.number_input("Factory Horsepower", 80, 1200, 300, step=10)
|
| 142 |
-
|
| 143 |
st.markdown("---")
|
| 144 |
-
st.subheader("Modifications")
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
induction = st.selectbox("Forced Induction", ["Naturally Aspirated", "Single Turbo", "Twin Turbo", "Supercharger"])
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
fuel = st.selectbox("Fuel Type", ["87 Octane", "91 Octane", "93 Octane", "E85 / Race Gas"])
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
map_tune = {"Stock Map":0, "Stage 1":1, "Stage 2":2, "Custom Dyno":3}
|
| 167 |
-
map_fuel = {"87 Octane":0, "91 Octane":1, "93 Octane":2, "E85 / Race Gas":3}
|
| 168 |
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
engine, cyl, base_hp,
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
map_exhaust[exhaust],
|
| 174 |
-
map_induct[induction],
|
| 175 |
-
1 if intercooler else 0,
|
| 176 |
-
map_fuel[fuel],
|
| 177 |
-
map_tune[tune]
|
| 178 |
]])
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
# ---------------------------
|
| 185 |
-
# 5. Main Dashboard (MacOS Style)
|
| 186 |
-
# ---------------------------
|
| 187 |
|
| 188 |
-
# Header
|
| 189 |
-
st.markdown("
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
-
#
|
| 194 |
-
col1, col2 = st.columns([1,
|
| 195 |
|
| 196 |
with col1:
|
| 197 |
-
#
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
with col2:
|
| 207 |
-
#
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
|
| 212 |
-
with m1:
|
| 213 |
-
st.metric("Base Power", f"{base_hp} HP")
|
| 214 |
-
with m2:
|
| 215 |
-
st.metric("Gain", f"+{pred_gain:.0f} HP", delta=f"{((pred_gain/base_hp)*100):.1f}%")
|
| 216 |
-
with m3:
|
| 217 |
-
st.metric("Total Power", f"{final_hp:.0f} HP", delta="Peak Output")
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
#
|
| 226 |
-
st.
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
peak_rpm = 6500
|
| 236 |
-
# simplified curve logic
|
| 237 |
-
return -((rpm - peak_rpm)**2) + (peak_hp * 5000)
|
| 238 |
-
|
| 239 |
-
# Normalized curves scaled to HP
|
| 240 |
-
base_curve = [base_hp * (1 - ((x - 6500)/5000)**2) * (x/8000) for x in rpms]
|
| 241 |
-
mod_curve = [final_hp * (1 - ((x - 6500)/5000)**2) * (x/8000) for x in rpms]
|
| 242 |
-
|
| 243 |
-
chart_data = pd.DataFrame({
|
| 244 |
-
"RPM": np.tile(rpms, 2),
|
| 245 |
-
"Horsepower": np.concatenate([base_curve, mod_curve]),
|
| 246 |
-
"Setup": ["Factory Stock"] * 50 + ["Modified"] * 50
|
| 247 |
-
})
|
| 248 |
-
|
| 249 |
-
# Use Streamlit's native area chart but configure it to look clean
|
| 250 |
-
st.area_chart(
|
| 251 |
-
chart_data,
|
| 252 |
-
x="RPM",
|
| 253 |
-
y="Horsepower",
|
| 254 |
-
color="Setup",
|
| 255 |
-
stack=False # Overlay them
|
| 256 |
-
)
|
| 257 |
-
|
| 258 |
-
# Footer / "Dock" feel
|
| 259 |
-
st.divider()
|
| 260 |
-
cols = st.columns(4)
|
| 261 |
-
cols[0].info(f"Fuel: {fuel}")
|
| 262 |
-
cols[1].info(f"Exhaust: {exhaust}")
|
| 263 |
-
cols[2].info(f"Headers: {'Yes' if headers else 'No'}")
|
| 264 |
-
cols[3].info(f"Intercooler: {'Yes' if intercooler else 'No'}")
|
| 265 |
-
|
|
|
|
| 4 |
from sklearn.preprocessing import StandardScaler
|
| 5 |
from sklearn.neighbors import KNeighborsRegressor
|
| 6 |
|
| 7 |
+
# ---------------------------------------------------------
|
| 8 |
+
# 1. macOS STYLING (FIXED FOR DARK MODE ISSUES)
|
| 9 |
+
# ---------------------------------------------------------
|
| 10 |
+
st.set_page_config(page_title="Mac-Mod Tuner", page_icon="π₯οΈ", layout="wide")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
|
|
|
| 12 |
st.markdown("""
|
| 13 |
+
<style>
|
| 14 |
+
/* FORCE TEXT COLOR TO BLACK (Overrides System Dark Mode) */
|
| 15 |
+
.stApp, .stMarkdown, p, h1, h2, h3, h4, h5, h6, span, div, label {
|
| 16 |
+
color: #1d1d1f !important;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* Main Background - Apple Light Grey */
|
| 20 |
.stApp {
|
| 21 |
+
background-color: #f5f5f7;
|
| 22 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 23 |
}
|
| 24 |
|
| 25 |
+
/* Sidebar Styling */
|
| 26 |
section[data-testid="stSidebar"] {
|
| 27 |
+
background-color: #e8e8ed;
|
| 28 |
+
border-right: 1px solid #d1d1d6;
|
| 29 |
}
|
| 30 |
|
| 31 |
+
/* Fix Sidebar Text Colors specifically */
|
| 32 |
+
section[data-testid="stSidebar"] * {
|
| 33 |
+
color: #1d1d1f !important;
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
+
/* Card/Container Styling */
|
| 37 |
+
div[data-testid="stVerticalBlock"] > div[style*="background-color"] {
|
| 38 |
+
background-color: white;
|
| 39 |
+
border-radius: 18px;
|
| 40 |
+
padding: 20px;
|
| 41 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
| 42 |
+
border: 1px solid #e5e5ea;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* Input Widgets - Fix text inside inputs */
|
| 46 |
+
.stSelectbox > div > div, .stNumberInput > div > div > input {
|
| 47 |
+
background-color: white !important;
|
| 48 |
+
color: #1d1d1f !important;
|
| 49 |
border-radius: 12px;
|
| 50 |
+
border: 1px solid #d1d1d6;
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
+
/* Dropdown menu text fix */
|
| 54 |
+
ul[data-testid="stSelectboxVirtualDropdown"] li {
|
| 55 |
+
color: black !important;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* Metrics Styling */
|
| 59 |
+
div[data-testid="stMetricValue"] {
|
| 60 |
+
font-weight: 700 !important;
|
| 61 |
+
color: #1d1d1f !important;
|
| 62 |
+
}
|
| 63 |
+
div[data-testid="stMetricLabel"] {
|
| 64 |
+
color: #86868b !important;
|
| 65 |
}
|
| 66 |
|
| 67 |
+
/* Custom "Traffic Lights" */
|
| 68 |
+
.traffic-lights {
|
| 69 |
+
display: flex;
|
| 70 |
+
gap: 8px;
|
| 71 |
+
margin-bottom: 20px;
|
|
|
|
| 72 |
}
|
| 73 |
+
.dot { width: 12px; height: 12px; border-radius: 50%; }
|
| 74 |
+
.red { background-color: #ff5f57; border: 1px solid #e0443e; }
|
| 75 |
+
.yellow { background-color: #febc2e; border: 1px solid #d89e24; }
|
| 76 |
+
.green { background-color: #28c840; border: 1px solid #1aab29; }
|
| 77 |
+
</style>
|
| 78 |
+
""", unsafe_allow_html=True)
|
| 79 |
+
|
| 80 |
+
# ---------------------------------------------------------
|
| 81 |
+
# 2. DATA & MODEL (V12 & NITROUS)
|
| 82 |
+
# ---------------------------------------------------------
|
| 83 |
|
|
|
|
|
|
|
|
|
|
| 84 |
@st.cache_resource
|
| 85 |
+
def train_model():
|
| 86 |
np.random.seed(42)
|
| 87 |
+
n = 1000
|
| 88 |
data = []
|
| 89 |
|
| 90 |
for _ in range(n):
|
| 91 |
+
engine = np.random.choice([1.6, 2.0, 2.4, 3.0, 3.8, 4.0, 5.0, 5.2, 6.0, 6.5])
|
| 92 |
+
cyl = np.random.choice([4, 6, 8, 10, 12])
|
| 93 |
+
base_hp = int(engine * np.random.uniform(60, 110))
|
| 94 |
|
| 95 |
+
intake = np.random.choice([0, 1, 2])
|
| 96 |
+
exhaust = np.random.choice([0, 1, 2, 3])
|
| 97 |
+
induction = np.random.choice([0, 1, 2, 3])
|
| 98 |
+
cams = np.random.choice([0, 1, 2])
|
| 99 |
+
nitrous = np.random.choice([0, 1])
|
| 100 |
+
fuel = np.random.choice([0, 1, 2, 3])
|
| 101 |
+
tune = np.random.choice([0, 1, 2])
|
| 102 |
|
| 103 |
+
gain = (
|
| 104 |
+
(intake * 3) +
|
| 105 |
+
(exhaust * 5) +
|
| 106 |
+
(induction * (base_hp * 0.35)) +
|
| 107 |
+
(cams * (base_hp * 0.10)) +
|
| 108 |
+
(nitrous * 50) +
|
| 109 |
+
(tune * (base_hp * 0.08)) +
|
| 110 |
+
(fuel * 4) +
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
np.random.uniform(-5, 5)
|
| 112 |
)
|
| 113 |
|
| 114 |
+
if cyl < 6 and gain > 200:
|
| 115 |
+
gain *= 0.8
|
|
|
|
| 116 |
|
| 117 |
+
data.append([engine, cyl, base_hp, intake, exhaust, induction, cams, nitrous, fuel, tune, gain])
|
| 118 |
|
| 119 |
+
columns = ["engine", "cyl", "base_hp", "intake", "exhaust", "induction", "cams", "nitrous", "fuel", "tune", "hp_gain"]
|
| 120 |
df = pd.DataFrame(data, columns=columns)
|
| 121 |
|
| 122 |
X = df.drop("hp_gain", axis=1)
|
|
|
|
| 130 |
|
| 131 |
return scaler, model
|
| 132 |
|
| 133 |
+
scaler, model = train_model()
|
| 134 |
+
|
| 135 |
+
# ---------------------------------------------------------
|
| 136 |
+
# 3. SIDEBAR CONTROLS
|
| 137 |
+
# ---------------------------------------------------------
|
| 138 |
|
|
|
|
|
|
|
|
|
|
| 139 |
with st.sidebar:
|
| 140 |
+
st.header("βοΈ Configuration")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
st.markdown("---")
|
|
|
|
| 142 |
|
| 143 |
+
with st.expander("π Base Vehicle Stats", expanded=True):
|
| 144 |
+
col_eng_1, col_eng_2 = st.columns(2)
|
| 145 |
+
with col_eng_1:
|
| 146 |
+
cyl = st.selectbox("Cylinders", [4, 5, 6, 8, 10, 12])
|
| 147 |
+
with col_eng_2:
|
| 148 |
+
engine = st.selectbox("Size (L)", [1.6, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0, 5.2, 6.0, 6.5, 8.4])
|
| 149 |
+
|
| 150 |
+
base_hp = st.number_input("Factory HP", 100, 1000, 300)
|
| 151 |
+
|
| 152 |
+
with st.expander("π§ Bolt-on Modifications", expanded=True):
|
| 153 |
+
intake = st.selectbox("Intake", ["Stock", "High Flow Filter", "Cold Air Intake"])
|
| 154 |
+
exhaust = st.selectbox("Exhaust", ["Stock", "Cat-back", "Long Tube Headers", "Full Straight Pipe"])
|
| 155 |
+
|
| 156 |
+
with st.expander("π₯ Internals & Boost", expanded=True):
|
| 157 |
induction = st.selectbox("Forced Induction", ["Naturally Aspirated", "Single Turbo", "Twin Turbo", "Supercharger"])
|
| 158 |
+
cams = st.selectbox("Camshafts", ["Stock", "Street Profile", "Track/Race Profile"])
|
| 159 |
+
nitrous = st.checkbox("Nitrous Oxide System (NOS)", value=False)
|
|
|
|
| 160 |
|
| 161 |
+
with st.expander("π» Tuning & Fuel", expanded=True):
|
| 162 |
+
fuel = st.selectbox("Fuel Type", ["87 Octane", "91 Octane", "93 Octane", "E85 (Ethanol)"])
|
| 163 |
+
tune = st.selectbox("ECU Map", ["Stock Map", "Stage 1", "Stage 2"])
|
| 164 |
+
|
| 165 |
+
# ---------------------------------------------------------
|
| 166 |
+
# 4. MAIN DASHBOARD
|
| 167 |
+
# ---------------------------------------------------------
|
|
|
|
|
|
|
| 168 |
|
| 169 |
+
# Mappings
|
| 170 |
+
intake_map = {"Stock":0, "High Flow Filter":1, "Cold Air Intake":2}
|
| 171 |
+
exhaust_map = {"Stock":0, "Cat-back":1, "Long Tube Headers":2, "Full Straight Pipe":3}
|
| 172 |
+
induction_map = {"Naturally Aspirated":0, "Single Turbo":1, "Twin Turbo":2, "Supercharger":3}
|
| 173 |
+
cams_map = {"Stock":0, "Street Profile":1, "Track/Race Profile":2}
|
| 174 |
+
nitrous_map = {False:0, True:1}
|
| 175 |
+
fuel_map = {"87 Octane":0, "91 Octane":1, "93 Octane":2, "E85 (Ethanol)":3}
|
| 176 |
+
tune_map = {"Stock Map":0, "Stage 1":1, "Stage 2":2}
|
| 177 |
+
|
| 178 |
+
# Logic
|
| 179 |
+
input_data = np.array([[
|
| 180 |
engine, cyl, base_hp,
|
| 181 |
+
intake_map[intake], exhaust_map[exhaust], induction_map[induction],
|
| 182 |
+
cams_map[cams], nitrous_map[nitrous], fuel_map[fuel], tune_map[tune]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
]])
|
| 184 |
|
| 185 |
+
pred = model.predict(scaler.transform(input_data))[0]
|
| 186 |
+
new_hp = base_hp + pred
|
| 187 |
+
pct_gain = (pred / base_hp) * 100
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
+
# UI Header
|
| 190 |
+
st.markdown("""
|
| 191 |
+
<div class="traffic-lights">
|
| 192 |
+
<div class="dot red"></div>
|
| 193 |
+
<div class="dot yellow"></div>
|
| 194 |
+
<div class="dot green"></div>
|
| 195 |
+
</div>
|
| 196 |
+
""", unsafe_allow_html=True)
|
| 197 |
+
|
| 198 |
+
st.title("Performance Estimator Pro")
|
| 199 |
+
st.markdown(f"Analysis for **{cyl}-Cylinder {engine}L Engine**")
|
| 200 |
+
st.divider()
|
| 201 |
|
| 202 |
+
# UI Body
|
| 203 |
+
col1, col2 = st.columns([1.5, 1])
|
| 204 |
|
| 205 |
with col1:
|
| 206 |
+
st.markdown("### π Dyno Projection")
|
| 207 |
+
chart_data = pd.DataFrame({
|
| 208 |
+
"Horsepower": [base_hp, new_hp],
|
| 209 |
+
"Stage": ["Factory", "Modified"]
|
| 210 |
+
})
|
| 211 |
+
|
| 212 |
+
st.vega_lite_chart(chart_data, {
|
| 213 |
+
"mark": {"type": "bar", "cornerRadiusEnd": 6, "color": "#007AFF"},
|
| 214 |
+
"encoding": {
|
| 215 |
+
"x": {"field": "Stage", "type": "nominal", "axis": {"labelAngle": 0, "labelColor": "#1d1d1f"}},
|
| 216 |
+
"y": {"field": "Horsepower", "type": "quantitative", "axis": {"labelColor": "#1d1d1f"}},
|
| 217 |
+
"tooltip": ["Stage", "Horsepower"]
|
| 218 |
+
}
|
| 219 |
+
}, use_container_width=True)
|
| 220 |
|
| 221 |
with col2:
|
| 222 |
+
st.markdown("### β‘ Results")
|
| 223 |
+
|
| 224 |
+
with st.container():
|
| 225 |
+
# Custom HTML Metric for better styling
|
| 226 |
+
st.markdown(f"""
|
| 227 |
+
<div style="background-color: white; border-radius: 12px; padding: 15px; border: 1px solid #e5e5ea;">
|
| 228 |
+
<div style="font-size: 14px; color: #86868b; text-transform: uppercase; font-weight: 600;">Total Power Output</div>
|
| 229 |
+
<div style="font-size: 42px; font-weight: 800; color: #1d1d1f; letter-spacing: -1px;">{int(new_hp)} HP</div>
|
| 230 |
+
</div>
|
| 231 |
+
""", unsafe_allow_html=True)
|
| 232 |
|
| 233 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
+
c1, c2 = st.columns(2)
|
| 236 |
+
with c1:
|
| 237 |
+
st.metric("Gain", f"+{int(pred)} HP")
|
| 238 |
+
with c2:
|
| 239 |
+
st.metric("Improvement", f"{pct_gain:.1f}%")
|
| 240 |
+
|
| 241 |
+
st.markdown("### π Build Summary")
|
| 242 |
+
with st.container():
|
| 243 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 244 |
+
c1.info(f"**Induction:** {induction}")
|
| 245 |
+
c2.info(f"**Fuel:** {fuel}")
|
| 246 |
+
c3.info(f"**Camshafts:** {cams}")
|
| 247 |
+
c4.info(f"**Nitrous:** {'Enabled' if nitrous else 'Disabled'}")
|
| 248 |
+
|
| 249 |
+
if pct_gain > 50:
|
| 250 |
+
st.toast("π Massive gains! Ensure engine internals are forged.", icon="β οΈ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|