From a457bea95358825e55dbc7f48d57183004121109 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 13 May 2026 22:39:50 -0700 Subject: Apply Prism design system — brass/ink palette, EB Garamond + IBM Plex typography MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the design kit: champagne brass accent (#C2AA7A), deep midnight backgrounds, EB Garamond italic headings, IBM Plex Sans/Mono body and numbers, terminal-density KPI cards, restyled sidebar brand mark, flat pill tabs, and a global Plotly theme so all charts inherit the dark palette automatically. Co-Authored-By: Claude Sonnet 4.6 --- .streamlit/config.toml | 8 +- app.py | 668 ++++++++++++++++++++++++++++++++++++++++------- components/filings.py | 35 ++- components/insiders.py | 10 +- components/market_bar.py | 54 ++-- components/options.py | 4 +- components/overview.py | 57 ++-- components/top_movers.py | 44 ++-- components/valuation.py | 14 +- 9 files changed, 697 insertions(+), 197 deletions(-) diff --git a/.streamlit/config.toml b/.streamlit/config.toml index e3fbcf9..16c0c8e 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,9 +1,9 @@ [theme] base = "dark" -primaryColor = "#4F8EF7" -backgroundColor = "#0E1117" -secondaryBackgroundColor = "#161C27" -textColor = "#FAFAFA" +primaryColor = "#C2AA7A" +backgroundColor = "#0B0E13" +secondaryBackgroundColor = "#11151C" +textColor = "#C7C0AE" font = "sans serif" [server] diff --git a/app.py b/app.py index d86475b..d5e082a 100644 --- a/app.py +++ b/app.py @@ -11,50 +11,414 @@ st.set_page_config( initial_sidebar_state="expanded", ) -# ── Global CSS ──────────────────────────────────────────────────────────────── +# ── Design system CSS ───────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) +import plotly.graph_objects as go +import plotly.io as pio + +# ── Plotly theme ────────────────────────────────────────────────────────────── +_prism_layout = go.Layout( + paper_bgcolor="#0B0E13", + plot_bgcolor="#0B0E13", + font=dict(family="IBM Plex Mono, SF Mono, Menlo, monospace", color="#C7C0AE", size=11), + title=dict( + font=dict(family="EB Garamond, Georgia, serif", color="#F2ECDC", size=18), + x=0, + ), + colorway=["#C2AA7A", "#4F8C5E", "#4A78B5", "#B5494B", "#C49545", "#8B7FBF"], + xaxis=dict( + gridcolor="#232934", + linecolor="#232934", + tickfont=dict(family="IBM Plex Mono, monospace", color="#5E5849", size=10), + title=dict(font=dict(color="#8E8676", size=11)), + showgrid=True, + zeroline=False, + ), + yaxis=dict( + gridcolor="#232934", + linecolor="#232934", + tickfont=dict(family="IBM Plex Mono, monospace", color="#5E5849", size=10), + title=dict(font=dict(color="#8E8676", size=11)), + showgrid=True, + zeroline=False, + ), + legend=dict( + bgcolor="rgba(17,21,28,0.85)", + bordercolor="#232934", + borderwidth=1, + font=dict(family="IBM Plex Sans, sans-serif", color="#C7C0AE", size=11), + ), + margin=dict(l=48, r=16, t=32, b=40), + hoverlabel=dict( + bgcolor="#181D26", + bordercolor="#2E3645", + font=dict(family="IBM Plex Mono, monospace", color="#F2ECDC", size=11), + ), +) +_prism_template = go.layout.Template(layout=_prism_layout) +pio.templates["prism"] = _prism_template +pio.templates.default = "prism" + from components.market_bar import render_market_bar from components.top_movers import render_top_movers from components.overview import render_overview @@ -71,21 +435,49 @@ if "ticker" not in st.session_state: st.session_state["ticker"] = None -# ── Sidebar ────────────────────────────────────────────────────────────────── +# ── Sidebar ─────────────────────────────────────────────────────────────────── with st.sidebar: - col_logo, col_title = st.columns([1, 2]) - with col_logo: - st.image("assets/logo.png", width=60) - with col_title: - st.markdown("### Prism") - st.caption("Financial Analysis Dashboard") - st.divider() + # Brand mark + st.markdown(""" +
+
P
+
+ Prism + Research Terminal +
+
+ """, unsafe_allow_html=True) with st.form("ticker_search_form", clear_on_submit=False): query = st.text_input( - "Search company or ticker", - placeholder="e.g. Apple, AAPL, MSFT…", + "Ticker or company", + placeholder="AAPL, Apple, MSFT…", key="search_query", ).strip() @@ -108,50 +500,117 @@ with st.sidebar: if submitted and selected_symbol: st.session_state["ticker"] = selected_symbol - if st.session_state["ticker"]: - st.caption(f"Currently viewing: **{st.session_state['ticker']}**") - ticker = st.session_state["ticker"] - # Quick company info in sidebar - st.divider() + # Company snapshot if ticker: + st.divider() info = get_company_info(ticker) - if ticker and info: - st.caption(info.get("longName", ticker)) - price = get_latest_price(ticker) - prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose") - if price is not None: - if prev_close and prev_close > 0: - chg = price - prev_close - chg_pct = chg / prev_close * 100 - sign = "+" if chg >= 0 else "" - color = "#2ecc71" if chg >= 0 else "#e74c3c" - st.markdown( - f"${price:,.2f}" - f" {sign}{chg:+.2f} ({sign}{chg_pct:.2f}%)", - unsafe_allow_html=True, - ) - else: - st.markdown( - f"${price:,.2f}", - unsafe_allow_html=True, - ) - _EXCHANGE_NAMES = { - "NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", - "NCM": "NASDAQ", "ASE": "AMEX", "PCX": "NYSE Arca", - "BTS": "BATS", "TSX": "TSX", "LSE": "LSE", - } - raw_exchange = info.get("exchange", "") - exchange = _EXCHANGE_NAMES.get(raw_exchange, raw_exchange) or "—" - st.caption(f"Exchange: {exchange}") - st.caption(f"Currency: {info.get('currency', 'USD')}") - st.caption(f"Sector: {info.get('sector', '—')}") - employees = info.get("fullTimeEmployees") - st.caption(f"Employees: {employees:,}" if isinstance(employees, int) else "Employees: —") - website = info.get("website") - if website: - st.markdown(f"[🌐 Website]({website})") + if info: + co_name = info.get("longName", ticker) + price = get_latest_price(ticker) + prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose") + + # Ticker + name + st.markdown(f""" +
+
{ticker}
+
{co_name}
+
+ """, unsafe_allow_html=True) + + # Price + change + if price is not None: + if prev_close and prev_close > 0: + chg = price - prev_close + chg_pct = chg / prev_close * 100 + sign = "+" if chg >= 0 else "" + px_color = "#4F8C5E" if chg >= 0 else "#B5494B" + st.markdown(f""" +
+ ${price:,.2f} + {sign}{chg_pct:.2f}% +
+ """, unsafe_allow_html=True) + else: + st.markdown(f""" +
+ ${price:,.2f} +
+ """, unsafe_allow_html=True) + + # Company meta + _EXCHANGE_NAMES = { + "NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", + "NCM": "NASDAQ", "ASE": "AMEX", "PCX": "NYSE Arca", + "BTS": "BATS", "TSX": "TSX", "LSE": "LSE", + } + raw_exchange = info.get("exchange", "") + exchange = _EXCHANGE_NAMES.get(raw_exchange, raw_exchange) or "—" + sector = info.get("sector", "—") + currency = info.get("currency", "USD") + employees = info.get("fullTimeEmployees") + emp_str = f"{employees:,}" if isinstance(employees, int) else "—" + + rows = [ + ("Exchange", exchange), + ("Sector", sector), + ("Currency", currency), + ("Employees", emp_str), + ] + rows_html = "".join(f""" +
+ {k} + {v} +
+ """ for k, v in rows) + + st.markdown(f""" +
{rows_html}
+ """, unsafe_allow_html=True) + + website = info.get("website", "") + if website: + st.markdown(f""" +
+ Website ↗ +
+ """, unsafe_allow_html=True) + + elif ticker: + st.caption(f"Viewing: **{ticker}**") # ── Market Bar ──────────────────────────────────────────────────────────────── @@ -171,17 +630,32 @@ st.divider() # ── Main Content ────────────────────────────────────────────────────────────── if not ticker: - st.info("Search for a company or ticker in the sidebar to get started.") + st.markdown(""" +
+
Search for a ticker to begin.
+
Enter a company name or symbol in the sidebar.
+
+ """, unsafe_allow_html=True) st.stop() tab_overview, tab_financials, tab_valuation, tab_options, tab_insiders, tab_filings, tab_news = st.tabs([ - "📈 Overview", - "📊 Financials", - "💰 Valuation", - "🎯 Options", - "👤 Insiders", - "📁 Filings", - "📰 News", + "Overview", + "Financials", + "Valuation", + "Options", + "Insiders", + "Filings", + "News", ]) with tab_overview: diff --git a/components/filings.py b/components/filings.py index a1e4417..9e3b156 100644 --- a/components/filings.py +++ b/components/filings.py @@ -17,9 +17,9 @@ _FORM_DESCRIPTIONS = { } _FORM_COLORS = { - "10-K": "rgba(79,142,247,0.15)", - "10-Q": "rgba(130,224,170,0.15)", - "8-K": "rgba(247,162,79,0.15)", + "10-K": "rgba(74,120,181,0.15)", + "10-Q": "rgba(79,140,94,0.15)", + "8-K": "rgba(196,149,69,0.15)", } @@ -74,18 +74,35 @@ def render_filings(ticker: str): left, right = st.columns([5, 1]) with left: st.markdown( - f"
" - f"{form}  ·  " - f"{date}
" - f"{title}" + f"
" + f"{form}" + f"{title}" + f"{date}" f"
", unsafe_allow_html=True, ) with right: - # Prefer the actual filing doc over the Yahoo index page doc_url = exhibits.get(form) or edgar_url if doc_url: st.markdown( - f"
🔗 View
", + f"
" + f"View ↗
", unsafe_allow_html=True, ) diff --git a/components/insiders.py b/components/insiders.py index 07bc3e3..bdb1818 100644 --- a/components/insiders.py +++ b/components/insiders.py @@ -83,18 +83,16 @@ def render_insiders(ticker: str): fig = go.Figure() fig.add_trace(go.Bar( x=months, y=[monthly[m]["Buy"] / 1e6 for m in months], - name="Buys", marker_color="#2ecc71", + name="Buys", marker_color="#4F8C5E", )) fig.add_trace(go.Bar( x=months, y=[-monthly[m]["Sell"] / 1e6 for m in months], - name="Sells", marker_color="#e74c3c", + name="Sells", marker_color="#B5494B", )) fig.update_layout( title="Monthly Insider Net Activity ($M)", barmode="relative", yaxis_title="Value ($M)", - plot_bgcolor="rgba(0,0,0,0)", - paper_bgcolor="rgba(0,0,0,0)", margin=dict(l=0, r=0, t=40, b=0), height=280, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), @@ -133,9 +131,9 @@ def render_insiders(ticker: str): def _color_type(row): if row["Type"] == "Buy": - return [""] * 3 + ["background-color: rgba(46,204,113,0.15)"] + [""] * 2 + return [""] * 3 + ["background-color: #15241A; color: #4F8C5E"] + [""] * 2 if row["Type"] == "Sell": - return [""] * 3 + ["background-color: rgba(231,76,60,0.15)"] + [""] * 2 + return [""] * 3 + ["background-color: #2A1517; color: #B5494B"] + [""] * 2 return [""] * len(row) st.dataframe( diff --git a/components/market_bar.py b/components/market_bar.py index 411b232..e3accc5 100644 --- a/components/market_bar.py +++ b/components/market_bar.py @@ -23,46 +23,38 @@ def render_market_bar(): """ """, unsafe_allow_html=True, diff --git a/components/options.py b/components/options.py index e9bf016..0acce31 100644 --- a/components/options.py +++ b/components/options.py @@ -104,7 +104,7 @@ def render_options(ticker: str): y=calls_atm["impliedVolatility"] * 100, name="Calls IV", mode="lines+markers", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), marker=dict(size=4), )) if not puts_atm.empty and "impliedVolatility" in puts_atm.columns: @@ -113,7 +113,7 @@ def render_options(ticker: str): y=puts_atm["impliedVolatility"] * 100, name="Puts IV", mode="lines+markers", - line=dict(color="#F7A24F", width=2), + line=dict(color="#C49545", width=2), marker=dict(size=4), )) if current_price: diff --git a/components/overview.py b/components/overview.py index 1bb65c2..9a0d162 100644 --- a/components/overview.py +++ b/components/overview.py @@ -118,22 +118,22 @@ def _score_card(info: dict) -> None: return color_map = { - "green": ("rgba(46,204,113,0.15)", "#7ce3a1"), - "yellow": ("rgba(243,156,18,0.15)", "#f0c040"), - "red": ("rgba(231,76,60,0.15)", "#ff8a8a"), - "neutral": ("rgba(255,255,255,0.05)", "#9aa0b0"), + "green": ("#15241A", "#4F8C5E"), + "yellow": ("#2A1F0F", "#C49545"), + "red": ("#2A1517", "#B5494B"), + "neutral": ("#181D26", "#5E5849"), } cards_html = "" for label, color, value, desc in signals: bg, fg = color_map[color] cards_html += ( - f'
' - f'
{label}
' - f'
{value}
' - f'
{desc}
' + f'
' + f'
{label}
' + f'
{value}
' + f'
{desc}
' f'
' ) @@ -159,23 +159,28 @@ def _render_52w_bar(info: dict) -> None: st.markdown( f""" -
-
- 52W Low: {fmt_currency(low)} - - {fmt_currency(price)}  ·  {pct:.0f}% of range +
+
+ {fmt_currency(low)} + + {fmt_currency(price)}  ·  {pct:.0f}% - 52W High: {fmt_currency(high)} + {fmt_currency(high)}
-
+
-
+ background:#C2AA7A;border-radius:999px;">
+
-
- +{from_low_pct:.1f}% above low - {to_high_pct:.1f}% below high +
+ +{from_low_pct:.1f}% from low + {to_high_pct:.1f}% to high
""", @@ -286,10 +291,10 @@ def _render_relative_chart(ticker: str, info: dict, period: str): y=subject_series.values, mode="lines", name=ticker.upper(), - line=dict(color="#4F8EF7", width=2.5), + line=dict(color="#C2AA7A", width=2.5), )) - palette = ["#7ce3a1", "#F7A24F", "#c084fc", "#ff8a8a", "#9ad1ff"] + palette = ["#7ce3a1", "#C49545", "#c084fc", "#ff8a8a", "#9ad1ff"] plotted = 1 for idx, label in enumerate(selected_labels): symbol = option_map[label] @@ -424,7 +429,7 @@ def render_overview(ticker: str): y=hist["Close"], mode="lines", name="Close", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), fill="tozeroy", fillcolor="rgba(79,142,247,0.08)", )) diff --git a/components/top_movers.py b/components/top_movers.py index 5589df6..db95592 100644 --- a/components/top_movers.py +++ b/components/top_movers.py @@ -28,30 +28,39 @@ def _inject_styles(): padding: 0.18rem 0; } .prism-mover-symbol { - font-size: 1rem; - font-weight: 700; + font-family: 'IBM Plex Sans', sans-serif; + font-size: 0.875rem; + font-weight: 600; + color: #F2ECDC; line-height: 1.1; } .prism-mover-name { - color: #9aa0b0; - font-size: 0.84rem; + font-family: 'IBM Plex Sans', sans-serif; + color: #8E8676; + font-size: 0.8125rem; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .prism-mover-price { - font-size: 0.98rem; + font-family: 'IBM Plex Mono', monospace; + font-variant-numeric: tabular-nums; + font-size: 0.8125rem; + color: #C7C0AE; line-height: 1.1; } .prism-mover-change { - font-size: 0.98rem; - font-weight: 600; + font-family: 'IBM Plex Mono', monospace; + font-variant-numeric: tabular-nums; + font-size: 0.8125rem; + font-weight: 500; line-height: 1.1; } .prism-mover-change-meta { - font-size: 0.74rem; - color: #9aa0b0; + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: #5E5849; margin-left: 0.2rem; } @media (max-width: 900px) { @@ -91,7 +100,7 @@ def _mover_row_html(q: dict) -> str: try: chg_f = float(chg_pct) - color = "#2ecc71" if chg_f >= 0 else "#e74c3c" + color = "#4F8C5E" if chg_f >= 0 else "#B5494B" sign = "+" if chg_f >= 0 else "" pct_str = f"{sign}{chg_f:.2f}%" except Exception: @@ -143,11 +152,16 @@ def _render_mover_tab(screen: str, state_key: str): @st.fragment def render_top_movers(): _inject_styles() - st.markdown("#### 🔥 Top Movers") - - tab_gainers, tab_losers, tab_active = st.tabs([ - "📈 Gainers", "📉 Losers", "⚡ Most Active" - ]) + st.markdown(""" +
Top Movers
+ """, unsafe_allow_html=True) + + tab_gainers, tab_losers, tab_active = st.tabs(["Gainers", "Losers", "Most Active"]) screens = { "gainers": "day_gainers", diff --git a/components/valuation.py b/components/valuation.py index 0095f41..a141846 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -586,7 +586,7 @@ def _render_dcf_model(ctx: dict): fig = go.Figure(go.Bar( x=years + ["Terminal Value"], y=[(v / 1e9) for v in discounted] + [terminal_pv / 1e9], - marker_color=["#4F8EF7"] * len(years) + ["#F7A24F"], + marker_color=["#C2AA7A"] * len(years) + ["#C49545"], text=[f"${v / 1e9:.2f}B" for v in discounted] + [f"${terminal_pv / 1e9:.2f}B"], textposition="outside", )) @@ -1235,7 +1235,7 @@ def _render_analyst_targets(ticker: str): st.write("") - colors = ["#2ecc71", "#82e0aa", "#f0b27a", "#e59866", "#e74c3c"] + colors = ["#4F8C5E", "#4F8C5E", "#C49545", "#8F7A50", "#B5494B"] fig = go.Figure(go.Bar( x=list(counts.keys()), y=list(counts.values()), @@ -1314,14 +1314,14 @@ def _render_earnings_history(ticker: str): y=df_chart["epsActual"], name="Actual EPS", mode="lines+markers", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), )) fig.add_trace(go.Scatter( x=df_chart.index.astype(str), y=df_chart["epsEstimate"], name="Estimated EPS", mode="lines+markers", - line=dict(color="#F7A24F", width=2, dash="dash"), + line=dict(color="#C49545", width=2, dash="dash"), )) fig.update_layout( title="EPS: Actual vs. Estimate", @@ -1351,7 +1351,7 @@ _HIST_RATIO_OPTIONS = { } _CHART_COLORS = [ - "#4F8EF7", "#F7A24F", "#2ecc71", "#e74c3c", + "#C2AA7A", "#C49545", "#4F8C5E", "#B5494B", "#9b59b6", "#1abc9c", "#f39c12", "#e67e22", ] @@ -1534,7 +1534,7 @@ def _render_forward_estimates(ticker: str): y=hist["epsActual"], name="EPS Actual", mode="lines+markers", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), )) if fwd_dates: @@ -1560,7 +1560,7 @@ def _render_forward_estimates(ticker: str): y=fwd_eps, name="EPS Est. (Avg)", mode="lines+markers", - line=dict(color="#F7A24F", width=2, dash="dash"), + line=dict(color="#C49545", width=2, dash="dash"), )) fig.update_layout( -- cgit v1.3-2-g0d8e