pandeydigant31's picture
Batch B: #12 blowby, #1 split plots, #13 modebar, #2 export
5f5369e verified
Raw
History Blame Contribute Delete
13.8 kB
"""Dark engineering control-room theme for Murphy Unified dashboard.
Provides CSS, JS, colour constants, Plotly template, and helper functions
shared by every tab.
"""
from __future__ import annotations
import plotly.graph_objects as go
import plotly.io as pio
# ── Colour palette ──────────────────────────────────────────────────────────
BG = "#1a1e24"
SURFACE = "#21262e"
SURFACE2 = "#272d37"
BORDER = "#333c48"
BORDER_BRIGHT = "#445060"
ACCENT = "#4a9eca"
ACCENT2 = "#4fc98a"
WARN = "#d4922a"
DANGER = "#c94a4a"
TEXT = "#b8c8d8"
TEXT_DIM = "#6a7e92"
TEXT_BRIGHT = "#d8e8f0"
TRACE_COLORS: list[str] = [ACCENT, ACCENT2, WARN, DANGER, "#7a8fca", "#ca7a9e"]
# ── Named semantic colors for tab-level use (avoid hardcoded hex in tabs) ──
ENERGY_POS = ACCENT2 # positive energy terms in bar charts
ENERGY_NEG = DANGER # negative energy terms
VALVE_ICV = ACCENT # ICV traces
VALVE_DCV = DANGER # DCV traces
# ── Google Fonts ────────────────────────────────────────────────────────────
FONTS_HTML = (
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?'
"family=JetBrains+Mono:wght@400;500;600&"
'family=DM+Sans:wght@400;500;600;700&display=swap">'
)
# ── CSS ─────────────────────────────────────────────────────────────────────
CSS = """
/* ── Murphy Unified β€” dark engineering theme ─────────────────────────── */
:root {
--bg: #1a1e24; --surface: #21262e; --surface2: #272d37;
--border: #333c48; --border-bright: #445060;
--accent: #4a9eca; --accent2: #4fc98a; --warn: #d4922a; --danger: #c94a4a;
--text: #b8c8d8; --text-dim: #6a7e92; --text-bright: #d8e8f0;
--mono: 'JetBrains Mono', monospace;
--body: 'DM Sans', sans-serif;
}
/* ── Plotly modebar β€” always visible on hover (issue #13) ──────────── */
.js-plotly-plot .modebar { opacity: 0.7 !important; }
.js-plotly-plot:hover .modebar { opacity: 1 !important; }
.js-plotly-plot .modebar-btn { font-size: 16px; }
/* ── body / dark-mode override ──────────────────────────────────────── */
body, .dark {
background: var(--bg) !important;
color: var(--text) !important;
font-family: var(--body) !important;
}
.gradio-container {
background: var(--bg) !important;
max-width: 100% !important;
}
.block, .gr-block, .gr-box, .gr-panel {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 4px !important;
}
input, textarea, select, .gr-input, .gr-text-input {
background: var(--surface2) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 4px !important;
font-family: var(--mono) !important;
}
button, .gr-button {
background: var(--surface2) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 4px !important;
font-family: var(--body) !important;
}
button.primary, .gr-button.primary {
background: var(--accent) !important;
color: var(--bg) !important;
border-color: var(--accent) !important;
}
/* ── Tab navigation ─────────────────────────────────────────────────── */
.tabs .tab-nav button, .tab-nav button {
font-family: var(--mono) !important;
font-size: 10px !important;
text-transform: uppercase !important;
letter-spacing: 2px !important;
color: var(--text-dim) !important;
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 4px !important;
padding: 8px 16px !important;
}
.tabs .tab-nav button.selected, .tab-nav button.selected {
background: var(--surface2) !important;
color: var(--accent) !important;
border-color: var(--border-bright) !important;
border-bottom: 2px solid var(--accent) !important;
}
/* ── Headings ───────────────────────────────────────────────────────── */
h1, h2, h3, h4, h5, h6 {
font-family: var(--body) !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 2px !important;
color: var(--text-dim) !important;
}
/* ── Tables ──────────────────────────────────────────────────────────── */
table {
background: var(--surface) !important;
border-collapse: collapse !important;
}
thead, th {
background: var(--surface2) !important;
font-family: var(--body) !important;
font-weight: 600 !important;
text-transform: uppercase !important;
color: var(--text-dim) !important;
border: 1px solid var(--border) !important;
}
td {
background: var(--surface) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
font-family: var(--mono) !important;
font-size: 12px !important;
}
/* ── Accordion ──────────────────────────────────────────────────────── */
.gr-accordion .label-wrap, .accordion .label-wrap {
font-family: var(--body) !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 2px !important;
color: var(--text-dim) !important;
}
/* ── Sliders ────────────────────────────────────────────────────────── */
.gr-slider label, label span {
font-family: var(--body) !important;
color: var(--text-dim) !important;
}
/* ── Dropdown / listbox ─────────────────────────────────────────────── */
.gr-dropdown, .gr-listbox, ul.options {
background: var(--surface2) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 4px !important;
}
.gr-dropdown li:hover, .gr-listbox li:hover, ul.options li:hover {
background: var(--surface) !important;
}
/* ── Footer hidden ──────────────────────────────────────────────────── */
footer { display: none !important; }
/* ── Metric cards (LEFT accent bar) ─────────────────────────────────── */
.metric-row {
display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0;
}
.metric-card {
flex: 1; min-width: 140px;
background: var(--surface2);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 4px;
padding: 8px 12px;
}
.metric-card.green { border-left-color: var(--accent2); }
.metric-card.amber { border-left-color: var(--warn); }
.metric-card.red { border-left-color: var(--danger); }
.metric-val {
font-family: var(--mono);
font-size: 18px;
color: var(--text-bright);
font-variant-numeric: tabular-nums;
}
.metric-label {
font-family: var(--body);
font-size: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 600;
}
/* ── Status bar ─────────────────────────────────────────────────────── */
.status-bar {
font-family: var(--mono);
font-size: 10px;
color: var(--text-dim);
border-top: 1px solid var(--border);
margin-top: 8px;
padding-top: 6px;
}
/* ── Checkboxes / radio ─────────────────────────────────────────────── */
input[type="checkbox"], input[type="radio"] {
accent-color: var(--accent) !important;
width: 16px !important;
height: 16px !important;
appearance: auto !important;
-webkit-appearance: auto !important;
opacity: 1 !important;
position: relative !important;
pointer-events: auto !important;
}
input[type="checkbox"]:checked, input[type="radio"]:checked {
background: var(--accent) !important;
border-color: var(--accent) !important;
}
/* Force native rendering over Gradio's custom styling */
.checkbox-item input, label input[type="checkbox"], label input[type="radio"] {
appearance: auto !important;
-webkit-appearance: auto !important;
opacity: 1 !important;
}
/* Selected item label highlight */
.checkbox-item.selected, label:has(input:checked) {
border-color: var(--accent) !important;
background: rgba(74, 158, 202, 0.15) !important;
color: var(--text-bright) !important;
}
/* ── Status pills ───────────────────────────────────────────────────── */
.pill {
display: inline-flex;
font-family: var(--mono);
font-size: 10px;
border-radius: 20px;
padding: 2px 10px;
align-items: center;
}
.pill.nominal {
background: rgba(79, 201, 138, 0.15);
color: var(--accent2);
}
.pill.warn {
background: rgba(212, 146, 42, 0.15);
color: var(--warn);
}
.pill.danger {
background: rgba(201, 74, 74, 0.15);
color: var(--danger);
}
"""
# ── JS β€” force dark mode ───────────────────────────────────────────────────
FORCE_DARK = """
function FORCE_DARK() {
document.body.classList.add('dark');
document.documentElement.style.colorScheme = 'dark';
/* Issue #13: ensure Plotly modebar (download/copy buttons) is visible */
const observer = new MutationObserver(() => {
document.querySelectorAll('.js-plotly-plot').forEach(plot => {
if (plot._fullLayout && !plot._modebarPatched) {
Plotly.relayout(plot, {'modebar.orientation': 'v'});
plot._modebarPatched = true;
}
});
});
observer.observe(document.body, {childList: true, subtree: true});
}
"""
# ── Metric card helper ──────────────────────────────────────────────────────
def metric_html(label: str, value: str, unit: str = "", accent: str = "") -> str:
"""Render a single metric card div.
Parameters
----------
label : str
Short uppercase label displayed beneath the value.
value : str
Primary numeric / text value.
unit : str, optional
Unit suffix appended after *value*.
accent : str, optional
Extra CSS class for the card, e.g. ``"green"``, ``"amber"``, ``"red"``.
Returns
-------
str
HTML string for one ``.metric-card``.
"""
cls = f"metric-card {accent}" if accent else "metric-card"
unit_span = f" <span style='font-size:12px;color:{TEXT_DIM}'>{unit}</span>" if unit else ""
return (
f'<div class="{cls}">'
f' <div class="metric-val">{value}{unit_span}</div>'
f' <div class="metric-label">{label}</div>'
f"</div>"
)
# ── Font family strings (for inline styles & Plotly) ───────────────────────
FONT_MONO = "JetBrains Mono, monospace"
FONT_BODY = "DM Sans, sans-serif"
def engine_banner(label: str, color: str = ACCENT) -> str:
"""Render a small engine-label banner for the left panel."""
return (
f'<div style="font-family:{FONT_MONO};font-size:11px;'
f'color:{color};letter-spacing:3px;text-transform:uppercase;'
f'padding:6px 0;border-bottom:1px solid {BORDER};margin-bottom:8px;">'
f'{label}</div>'
)
# ── Plotly template ─────────────────────────────────────────────────────────
_axis = dict(
gridcolor="rgba(40,55,75,0.9)",
linecolor=BORDER_BRIGHT,
zeroline=False,
tickfont=dict(family=FONT_MONO, size=10, color=TEXT_DIM),
)
_template = go.layout.Template(
layout=go.Layout(
paper_bgcolor=SURFACE,
plot_bgcolor=SURFACE,
font=dict(family=FONT_BODY, color=TEXT),
title=dict(font=dict(family=FONT_BODY, color=TEXT_DIM)),
xaxis=_axis,
yaxis=_axis,
colorway=TRACE_COLORS,
margin=dict(l=56, r=16, t=44, b=44),
modebar=dict(
bgcolor="rgba(0,0,0,0)",
color=TEXT_DIM,
activecolor=ACCENT,
orientation="v",
),
)
)
# Plotly config applied to all figures β€” ensures modebar with download/copy
# button is visible (issue #13).
PLOTLY_CONFIG = {
"displayModeBar": True,
"modeBarButtonsToAdd": ["toImage"],
"toImageButtonOptions": {
"format": "png",
"filename": "murphy_plot",
"scale": 2,
},
}
pio.templates["murphy"] = _template
pio.templates.default = "murphy"