|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import datetime |
|
|
import traceback |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_number(num): |
|
|
if num is None: |
|
|
return "-" |
|
|
try: |
|
|
return f"{float(num):,.2f}".rstrip("0").rstrip(".") |
|
|
except: |
|
|
return str(num) |
|
|
|
|
|
def format_large_number(num): |
|
|
if num is None: |
|
|
return "-" |
|
|
try: |
|
|
n = float(num) |
|
|
if abs(n) >= 1_00_00_000: |
|
|
return f"{n/1_00_00_000:.2f} Cr" |
|
|
elif abs(n) >= 1_00_000: |
|
|
return f"{n/1_00_000:.2f} L" |
|
|
elif abs(n) >= 1_000: |
|
|
return f"{n/1_000:.2f} K" |
|
|
else: |
|
|
return format_number(n) |
|
|
except: |
|
|
return str(num) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def html_card(title, content): |
|
|
return f""" |
|
|
<div style=" |
|
|
background:#fff; |
|
|
border-radius:12px; |
|
|
padding:18px; |
|
|
margin:15px 0; |
|
|
box-shadow:0 2px 8px rgba(0,0,0,0.1); |
|
|
border-left:6px solid #0077cc; |
|
|
"> |
|
|
<h2 style="margin-top:0;color:#0077cc;">{title}</h2> |
|
|
<div>{content}</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
def html_section(title, content): |
|
|
return f""" |
|
|
<div style="margin:20px 0;"> |
|
|
<h3 style="color:#444;margin-bottom:8px;">{title}</h3> |
|
|
{content} |
|
|
</div> |
|
|
""" |
|
|
|
|
|
def html_error(msg): |
|
|
return f""" |
|
|
<div style=" |
|
|
padding:15px; |
|
|
margin:15px 0; |
|
|
background:#ffe6e6; |
|
|
border-left:6px solid #d9534f; |
|
|
border-radius:8px; |
|
|
color:#b30000; |
|
|
"> |
|
|
<b>Error:</b> {msg} |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean_df(df): |
|
|
if isinstance(df.index, pd.DatetimeIndex): |
|
|
df.index = df.index.strftime("%Y-%m-%d") |
|
|
df.replace([np.inf, -np.inf], np.nan, inplace=True) |
|
|
df.fillna("-", inplace=True) |
|
|
return df |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_table(df): |
|
|
try: |
|
|
df = df.copy() |
|
|
df = clean_df(df) |
|
|
html = df.to_html(classes="styled-table", escape=False, border=0) |
|
|
return f""" |
|
|
<style> |
|
|
.styled-table {{ |
|
|
width:100%; |
|
|
border-collapse:collapse; |
|
|
font-size:14px; |
|
|
}} |
|
|
.styled-table th {{ |
|
|
background:#0077cc; |
|
|
color:white; |
|
|
padding:8px; |
|
|
text-align:left; |
|
|
}} |
|
|
.styled-table td {{ |
|
|
padding:8px; |
|
|
border-bottom:1px solid #ddd; |
|
|
}} |
|
|
.styled-table tr:nth-child(even) {{ |
|
|
background:#f3f7ff; |
|
|
}} |
|
|
.styled-table tr:hover {{ |
|
|
background:#e7f1ff; |
|
|
}} |
|
|
</style> |
|
|
{html} |
|
|
""" |
|
|
except Exception as e: |
|
|
return html_error(f"Table render failed: {e}<br><pre>{traceback.format_exc()}</pre>") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def wrap_plotly_html(html_chart, table_html=None): |
|
|
extra = f"<div style='margin-top:20px'>{table_html}</div>" if table_html else "" |
|
|
return f""" |
|
|
<div style="width:98%;margin:auto;"> |
|
|
{html_card("Chart", html_chart)} |
|
|
{extra} |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def safe_get(df, key, default_val="-"): |
|
|
try: |
|
|
return df.get(key, default_val) |
|
|
except: |
|
|
return default_val |
|
|
|
|
|
def format_timestamp_to_date(timestamp): |
|
|
if not isinstance(timestamp, (int, float)) or timestamp <= 0: |
|
|
return "N/A" |
|
|
try: |
|
|
return datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d') |
|
|
except: |
|
|
return "Invalid Date" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
STYLE_BLOCK = """ |
|
|
<style> |
|
|
.styled-table {border-collapse: collapse; margin: 10px 0; font-size: 0.9em; font-family: sans-serif; width: 100%; box-shadow: 0 0 10px rgba(0,0,0,0.1);} |
|
|
.styled-table th, .styled-table td {padding: 8px 10px; border: 1px solid #ddd;} |
|
|
.styled-table tbody tr:nth-child(even) {background-color: #f9f9f9;} |
|
|
.card {display: block; width: 95%; margin: 10px auto; padding: 15px; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); background: #fafafa;} |
|
|
.card-category-title {font-size: 1.1em; color: #222; margin: 0 0 8px; border-bottom: 1px solid #eee; padding-bottom: 5px;} |
|
|
.card-content-grid {display: flex; flex-wrap: wrap; gap: 15px;} |
|
|
.key-value-pair {flex: 1 1 calc(20% - 15px); box-sizing: border-box; min-width: 150px; background: #fff; padding: 10px; border: 1px solid #e0e0e0; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.05);} |
|
|
.key-value-pair h3 {font-size: 0.95em; color: #444; margin: 0 0 5px 0;} |
|
|
.key-value-pair p {font-size: 0.9em; color: #555; margin: 0; font-weight: bold;} |
|
|
.big-box {width:95%; margin:20px auto; padding:20px; border:1px solid #ccc; border-radius:8px; background:#fff; box-shadow:0 2px 8px rgba(0,0,0,0.1); font-size:0.95em; line-height:1.4em; max-height:400px; overflow-y:auto;} |
|
|
</style> |
|
|
""" |
|
|
|
|
|
def wrap_html(content, title="Stock Data"): |
|
|
return f""" |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>{title}</title> |
|
|
{STYLE_BLOCK} |
|
|
</head> |
|
|
<body> |
|
|
{content} |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|