| import streamlit as st |
| import pandas as pd |
| import json, time |
| from datetime import datetime |
| from streamlit_cookies_controller import CookieController |
| import streamlit.components.v1 as components |
| from gs_client import read_ws |
|
|
| st.set_page_config(layout="wide", initial_sidebar_state="collapsed", page_title="PMJA - Dashboard Expedição") |
|
|
| st.markdown("""<style> |
| #MainMenu,footer,header,[data-testid="stToolbar"],[data-testid="stDecoration"], |
| [data-testid="stStatusWidget"]{display:none!important} |
| .block-container,.element-container,.stMarkdown{padding:0!important;margin:0!important;max-width:100%!important} |
| iframe{position:fixed!important;top:0!important;left:0!important; |
| width:100vw!important;height:100dvh!important;border:none!important;z-index:9999!important} |
| [data-testid="stHorizontalBlock"]{gap:0!important;margin:0!important;padding:0!important; |
| height:0!important;overflow:visible!important} |
| .main,[data-testid="stAppViewContainer"]{background:#f5f7fa!important} |
| @media(max-width:900px){ |
| iframe{position:static!important;width:100%!important;height:7500px!important;z-index:1!important} |
| .main,[data-testid="stAppViewContainer"]{overflow-y:auto!important;height:auto!important} |
| .block-container{height:auto!important} |
| } |
| </style>""", unsafe_allow_html=True) |
|
|
| |
| _cc = CookieController() |
| COOKIE_NAME = "pmja_session" |
|
|
| def load_session(): |
| if st.session_state.get("logged_in") and st.session_state.get("session_exp"): |
| if datetime.now() < st.session_state.session_exp: |
| return True |
| st.session_state.logged_in = False |
| return False |
| try: |
| c = _cc.get(COOKIE_NAME) |
| if not c: return False |
| if datetime.now() >= datetime.fromisoformat(c["expiry"]): |
| _cc.remove(COOKIE_NAME); return False |
| st.session_state.update(logged_in=True, session_uid=c["user_id"], |
| session_usr=c["username"], session_exp=datetime.fromisoformat(c["expiry"])) |
| return True |
| except Exception: |
| return False |
|
|
| if not load_session(): |
| st.switch_page("app.py") |
|
|
| if 'inicio_exibicao' not in st.session_state: |
| st.session_state.inicio_exibicao = time.time() |
| if time.time() - st.session_state.inicio_exibicao >= 120: |
| st.session_state.inicio_exibicao = time.time() |
| st.switch_page("pages/full.py") |
|
|
| |
| loading_placeholder = st.empty() |
| loading_placeholder.markdown(""" |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700;800&family=Sora:wght@800&display=swap" rel="stylesheet"> |
| <style> |
| #ld{position:fixed;inset:0;z-index:999999;background:linear-gradient(135deg,#003a70,#0056A3); |
| display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'DM Sans',sans-serif} |
| #ld .lg{display:flex;align-items:center;gap:24px;margin-bottom:36px} |
| #ld img{height:34px;filter:brightness(0) invert(1);opacity:.9} |
| #ld .dv{width:1px;height:32px;background:rgba(255,255,255,.25)} |
| #ld h2{font-family:'Sora',sans-serif;font-size:clamp(15px,4vw,21px);color:#fff;font-weight:800;margin-bottom:4px;text-align:center} |
| #ld p{font-size:clamp(11px,3vw,13px);color:rgba(255,255,255,.7);font-weight:600;margin-bottom:40px;text-align:center} |
| #ld .sp{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:#fff; |
| border-radius:50%;animation:sp .8s linear infinite;margin-bottom:24px} |
| @keyframes sp{to{transform:rotate(360deg)}} |
| #ld .bw{width:min(260px,70vw);height:4px;background:rgba(255,255,255,.15);border-radius:99px;overflow:hidden;margin-bottom:14px} |
| #ld .bf{height:100%;width:0%;background:#fff;border-radius:99px;transition:width .4s ease} |
| #ld .st{font-size:11px;color:rgba(255,255,255,.6);font-weight:600;text-align:center} |
| #ld .dt span{animation:dt 1.2s infinite;opacity:0} |
| #ld .dt span:nth-child(2){animation-delay:.2s} |
| #ld .dt span:nth-child(3){animation-delay:.4s} |
| @keyframes dt{0%,100%{opacity:0}50%{opacity:1}} |
| </style> |
| <div id="ld"> |
| <div class="lg"> |
| <img src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490"> |
| <div class="dv"></div> |
| <img src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg"> |
| </div> |
| <div class="sp"></div> |
| <h2>PMJA — Dashboard Expedição</h2> |
| <p>Gestão de Materiais</p> |
| <div class="bw"><div class="bf" id="ldbf"></div></div> |
| <div class="st" id="ldst">Conectando ao Google Sheets<span class="dt"><span>.</span><span>.</span><span>.</span></span></div> |
| </div> |
| <script> |
| (function(){ |
| var s=[{p:18,m:'Conectando ao Google Sheets'},{p:40,m:'Carregando dados de expedição'}, |
| {p:62,m:'Processando métricas'},{p:80,m:'Montando gráficos'},{p:95,m:'Quase pronto'}]; |
| var i=0; |
| function n(){ |
| if(i>=s.length)return; |
| var x=s[i++]; |
| var b=document.getElementById('ldbf'),t=document.getElementById('ldst'); |
| if(b)b.style.width=x.p+'%'; |
| if(t)t.innerHTML=x.m+'<span class="dt"><span>.</span><span>.</span><span>.</span></span>'; |
| setTimeout(n,950+Math.random()*550); |
| } |
| setTimeout(n,300); |
| })(); |
| </script> |
| """, unsafe_allow_html=True) |
|
|
| |
| df_exp_raw = read_ws("exp_dados") |
| loading_placeholder.empty() |
|
|
| |
| def fmt(n): |
| if pd.isna(n): return "0" |
| return "{:,}".format(int(n)).replace(',', '.') |
|
|
| def extrair_categoria(col): |
| for s in [' Requisições',' Requisiçoes',' Unid. Itens',' Unid Itens', |
| ' Itens por unidade',' Items por unidade']: |
| col = col.replace(s, '') |
| return col.strip() |
|
|
| def proc_exp(df_raw): |
| if df_raw is None or df_raw.empty: return None |
| df = df_raw.copy() |
| df.rename(columns={df.columns[0]: 'mes'}, inplace=True) |
| df = df[df['mes'].notna()].copy() |
| df['data'] = pd.to_datetime(df['mes'], format='%m/%Y', errors='coerce') |
| df = df.dropna(subset=['data']).copy() |
| df['ano'] = df['data'].dt.year |
| df['mes_num'] = df['data'].dt.month |
| req, unid, itens = [], [], [] |
| for col in df.columns: |
| c = col.lower().strip().replace('.','').replace(' ',' ') |
| if 'requisi' in c and 'unid' not in c and 'por' not in c: req.append(col) |
| if 'unid' in c and 'iten' in c and 'por' not in c: unid.append(col) |
| if 'iten' in c and 'por' in c and 'unid' in c: itens.append(col) |
| for col in req+unid+itens: |
| if col in df.columns: df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0) |
| df['qtd_requisicoes'] = df[req].sum(axis=1) |
| df['qtd_unidades_emitidas'] = df[unid].sum(axis=1) |
| df['qtd_itens_total'] = df[itens].sum(axis=1) if itens else 0 |
| df_final = df[['mes_num','ano','qtd_requisicoes','qtd_unidades_emitidas','qtd_itens_total']]\ |
| .sort_values(['ano','mes_num']).reset_index(drop=True) |
| df_det = df[['mes_num','ano']+req+unid+itens]\ |
| .sort_values(['ano','mes_num']).reset_index(drop=True) |
| return df_final, df_det, req, unid, itens |
|
|
| |
| def build_frames(df_plot, df_det, req_cols, unid_cols, itens_cols, anos_unicos, mes_max, CORES_EXP): |
| MESES_PT = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun', |
| 7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'} |
| metricas_linha = [ |
| {'col':'qtd_requisicoes', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef']}, |
| {'col':'qtd_unidades_emitidas','cores':['#001f3f','#0056A3','#00a8e8','#7ecfef']}, |
| {'col':'qtd_itens_total', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef']}, |
| ] |
| tipos_l1 = ['barra_h','barra_v','linha'] |
| frames = [] |
| for mes_atual in range(1, mes_max+1): |
| frame = {"mes": mes_atual, "cards": {}, "linha1": [], "linha2": []} |
| df_ate = df_plot[df_plot['mes_num'] <= mes_atual] |
| df_det_ate = df_det[df_det['mes_num'] <= mes_atual] |
| for col, tit, cor, suf in [ |
| ('qtd_requisicoes', 'Requisições', '#003a70',''), |
| ('qtd_unidades_emitidas','Unidades Emitidas','#005a9c',''), |
| ('qtd_itens_total', 'Total de Itens', '#0075be',''), |
| ]: |
| frame["cards"][col] = {"valor": fmt(df_ate[col].sum()), "cor": cor, "titulo": tit, "sufixo": suf} |
| meses_ex = sorted(df_plot[df_plot['mes_num'] <= mes_atual]['mes_num'].unique()) |
| for idx_m, cfg in enumerate(metricas_linha): |
| col = cfg['col'] |
| traces = [] |
| for idx_ano, ano in enumerate(anos_unicos): |
| d = df_plot[(df_plot['ano']==ano)&(df_plot['mes_num']<=mes_atual)].sort_values('mes_num') |
| if d.empty: continue |
| cor = cfg['cores'][idx_ano % len(cfg['cores'])] |
| vals, texts = [], [] |
| for m in meses_ex: |
| row = d[d['mes_num']==m] |
| v = float(row[col].iloc[0]) if not row.empty else 0.0 |
| vals.append(v); texts.append(fmt(v) if v > 0 else "") |
| traces.append({ |
| "ano": str(ano), "cor": cor, |
| "meses": meses_ex, "y": vals, "text": texts, |
| "fill_rgba": "rgba({},{},{},0.1)".format(int(cor[1:3],16),int(cor[3:5],16),int(cor[5:7],16)) |
| }) |
| frame["linha1"].append({"tipo": tipos_l1[idx_m], "traces": traces, "meses_ex": meses_ex}) |
| for grp in [ |
| {'cols': req_cols, 'tipo': 'donut'}, |
| {'cols': unid_cols, 'tipo': 'linha_sis'}, |
| {'cols': itens_cols, 'tipo': 'barv_sis'}, |
| ]: |
| totais = {} |
| for c in grp['cols']: |
| cat = extrair_categoria(c) |
| if cat not in totais: |
| totais[cat] = {'v': 0.0, 'cor': CORES_EXP[len(totais) % len(CORES_EXP)]} |
| totais[cat]['v'] += float(df_det_ate[c].sum()) if c in df_det_ate.columns else 0.0 |
| dados = sorted([(k,v) for k,v in totais.items() if v['v']>0], |
| key=lambda x: x[1]['v'], reverse=True) |
| frame["linha2"].append({ |
| "tipo": grp['tipo'], |
| "labels": [d[0] for d in dados], |
| "valores":[d[1]['v'] for d in dados], |
| "cores": [d[1]['cor'] for d in dados], |
| }) |
| frames.append(frame) |
| return frames |
|
|
| |
| result = proc_exp(df_exp_raw) if df_exp_raw is not None and not df_exp_raw.empty else None |
|
|
| if result is not None: |
| df_expedicao, df_det, req_cols, unid_cols, itens_cols = result |
| for col in ['qtd_requisicoes','qtd_unidades_emitidas','qtd_itens_total']: |
| df_expedicao[col] = pd.to_numeric(df_expedicao[col], errors='coerce').fillna(0) |
| mes_max = int(df_expedicao['mes_num'].max()) |
| anos_unicos = sorted(df_expedicao['ano'].unique()) |
| CORES_EXP = ['#001a33','#002d4d','#003d66','#004d80','#005d99','#006db3', |
| '#007dcc','#1a8dd4','#3399dd','#4da6e6','#66b3ee','#80c0f5','#99ccff','#b3d9ff'] |
| MESES_PT = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun', |
| 7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'} |
| metricas_cfg = [ |
| {'col':'qtd_requisicoes', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'suf':''}, |
| {'col':'qtd_unidades_emitidas','cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'suf':''}, |
| {'col':'qtd_itens_total', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'suf':''}, |
| ] |
|
|
| def sub_anos(cfg): |
| parts = [] |
| for i, ano in enumerate(anos_unicos): |
| cor = cfg['cores'][i % len(cfg['cores'])] |
| total = fmt(df_expedicao[df_expedicao['ano']==ano][cfg['col']].sum()) |
| parts.append("<span style='color:{};font-weight:700'>{}: {}{}</span>".format(cor, ano, total, cfg['suf'])) |
| return " <span style='color:#666'>|</span> ".join(parts) |
|
|
| def sub_sistemas(cols): |
| totais = {} |
| for c in cols: |
| cat = extrair_categoria(c) |
| if cat not in totais: totais[cat] = {'v':0.0,'cor':CORES_EXP[len(totais)%len(CORES_EXP)]} |
| totais[cat]['v'] += float(df_det[c].sum()) if c in df_det.columns else 0.0 |
| dados = sorted([(k,v) for k,v in totais.items() if v['v']>0], |
| key=lambda x: x[1]['v'], reverse=True) |
| return " <span style='color:#666'>|</span> ".join( |
| ["<span style='color:{};font-weight:700'>{}: {}</span>".format(d[1]['cor'], d[0], fmt(d[1]['v'])) |
| for d in dados]) |
|
|
| frames = build_frames(df_expedicao, df_det, req_cols, unid_cols, itens_cols, anos_unicos, mes_max, CORES_EXP) |
|
|
| import numpy as np |
| class NpEncoder(json.JSONEncoder): |
| def default(self, obj): |
| if isinstance(obj, (np.integer,)): return int(obj) |
| if isinstance(obj, (np.floating,)): return float(obj) |
| if isinstance(obj, np.ndarray): return obj.tolist() |
| return super().default(obj) |
|
|
| frames_json = json.dumps(frames, cls=NpEncoder, ensure_ascii=False) |
| meses_json = json.dumps(MESES_PT) |
| TITULOS_L1 = ['Requisições','Unidades Emitidas','Total de Itens'] |
| TITULOS_L2 = ['Requisições por Sistema','Unidades Emitidas por Sistema','Total de Itens por Sistema'] |
| subs_l1 = [sub_anos(c) for c in metricas_cfg] |
| subs_l2 = [sub_sistemas(req_cols), sub_sistemas(unid_cols), sub_sistemas(itens_cols)] |
|
|
| html = """<!DOCTYPE html> |
| <html> |
| <head> |
| <meta name="viewport" content="width=device-width,initial-scale=1.0"> |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800;900&display=swap" rel="stylesheet"> |
| <script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script> |
| <style> |
| * { margin:0; padding:0; box-sizing:border-box; } |
| html, body { width:100%; height:100%; overflow:hidden; |
| background:linear-gradient(135deg,#f5f7fa 0%,#e8eef5 100%); |
| font-family:'DM Sans',sans-serif; } |
| .root { display:grid; width:100%; height:100dvh; gap:3px; padding:4px; overflow:hidden; |
| grid-template-rows:auto auto 1fr 1fr; } |
| .hdr { background:linear-gradient(135deg,#003a70 0%,#0056A3 100%); |
| border-radius:8px; padding:4px 14px; display:flex; align-items:center; |
| justify-content:space-between; box-shadow:0 4px 12px rgba(0,58,112,.12); } |
| .logo { height:clamp(22px,3.2dvh,36px); filter:brightness(0) invert(1); opacity:.92; } |
| .ht { font-family:'Sora',sans-serif; font-size:clamp(12px,1.8dvh,20px); |
| color:#fff; font-weight:800; letter-spacing:-.5px; } |
| .hs { font-family:'DM Sans',sans-serif; font-size:clamp(9px,1.2dvh,14px); |
| color:rgba(255,255,255,.9); font-weight:700; } |
| .cards { display:grid; grid-template-columns:repeat(3,1fr); gap:3px; } |
| .mc { background:linear-gradient(135deg,#ffffff 0%,#f8fafd 100%); |
| border-radius:8px; padding:clamp(4px,.6dvh,10px) clamp(6px,.7vw,14px); |
| border-left:4px solid; box-shadow:0 2px 12px rgba(0,58,112,.08); overflow:hidden; } |
| .ml { font-family:'DM Sans',sans-serif; font-size:clamp(7px,.85dvh,11px); |
| color:#666; font-weight:700; text-transform:uppercase; |
| letter-spacing:.3px; margin-bottom:2px; white-space:nowrap; } |
| .mv { font-family:'Sora',sans-serif; font-size:clamp(13px,2dvh,26px); |
| font-weight:800; line-height:1; white-space:nowrap; } |
| .row3 { display:grid; grid-template-columns:repeat(3,1fr); gap:3px; min-height:0; } |
| .cb { background:white; border-radius:10px; |
| box-shadow:0 2px 12px rgba(0,58,112,.07),0 1px 3px rgba(0,58,112,.04); |
| overflow:visible; min-height:0; display:flex; flex-direction:column; |
| border:1px solid rgba(0,58,112,.04); } |
| .ct { background:linear-gradient(180deg,#fafbfd 0%,#ffffff 100%); |
| padding:4px 8px 3px; border-radius:10px 10px 0 0; |
| border-bottom:1px solid #eef1f6; flex-shrink:0; } |
| .ct-title { font-family:'Sora',sans-serif; font-size:clamp(9px,1.15dvh,15px); |
| color:#1a1a1a; font-weight:800; text-align:center; } |
| .ct-sub { text-align:center; font-size:clamp(7px,.8dvh,10px); |
| font-weight:700; margin-top:1px; line-height:1.3; font-family:'DM Sans',sans-serif; } |
| .cp { flex:1; min-height:0; overflow:visible; } |
| @media(max-width:900px){ |
| html,body { overflow:hidden!important; } |
| .root { height:100dvh!important; overflow:hidden!important; } |
| .cards { grid-template-columns:repeat(3,1fr)!important; } |
| .row3 { grid-template-columns:repeat(2,1fr)!important; } |
| .cb { min-height:0!important; } |
| .cp { min-height:0!important; } |
| } |
| @media(max-width:600px){ |
| html,body { overflow-y:auto!important; height:auto!important; } |
| .root { height:auto!important; overflow:visible!important; grid-template-rows:auto!important; } |
| .cards { grid-template-columns:1fr 1fr!important; } |
| .row3 { grid-template-columns:1fr!important; } |
| .cb { height:260px!important; } |
| .cp { height:210px!important; flex:none!important; } |
| .hs { display:none; } |
| .hdr { padding:4px 8px!important; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="root"> |
| <div class="hdr"> |
| <img class="logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ"> |
| <div style="text-align:center;flex:1"> |
| <div class="ht">PMJA - Dashboard Gestão de Materiais</div> |
| <div class="hs">Evolução Temporal - Expedição</div> |
| </div> |
| <img class="logo" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg" alt="Engie"> |
| </div> |
| <div class="cards" id="cards-row"></div> |
| <div class="row3" id="row1"></div> |
| <div class="row3" id="row2"></div> |
| </div> |
| <script> |
| var FRAMES = __FRAMES__; |
| var MESES = __MESES__; |
| var TITULOS_L1 = __TITULOS_L1__; |
| var TITULOS_L2 = __TITULOS_L2__; |
| var SUBS_L1 = __SUBS_L1__; |
| var SUBS_L2 = __SUBS_L2__; |
| |
| var CARDS_CFG = [ |
| {col:'qtd_requisicoes', titulo:'Requisições', cor:'#003a70', sufixo:''}, |
| {col:'qtd_unidades_emitidas', titulo:'Unidades Emitidas', cor:'#005a9c', sufixo:''}, |
| {col:'qtd_itens_total', titulo:'Total de Itens', cor:'#0075be', sufixo:''} |
| ]; |
| |
| var FRAME_MS = 700; |
| var PAUSE_MS = 60000; |
| var frameIdx = 0; |
| var animTimer = null; |
| var cdownTimer= null; |
| var inited = {}; |
| |
| function buildDOM() { |
| var cr = document.getElementById('cards-row'); |
| CARDS_CFG.forEach(function(c) { |
| cr.innerHTML += '<div class="mc" style="border-left-color:'+c.cor+'">' |
| + '<div class="ml">'+c.titulo+'</div>' |
| + '<div class="mv" id="mv_'+c.col+'" style="color:'+c.cor+'">0</div></div>'; |
| }); |
| var r1 = document.getElementById('row1'); |
| for (var i=0; i<3; i++) { |
| r1.innerHTML += '<div class="cb"><div class="ct">' |
| + '<div class="ct-title">'+TITULOS_L1[i]+'</div>' |
| + '<div class="ct-sub">'+SUBS_L1[i]+'</div></div>' |
| + '<div class="cp" id="l1_'+i+'"></div></div>'; |
| } |
| var r2 = document.getElementById('row2'); |
| for (var i=0; i<3; i++) { |
| r2.innerHTML += '<div class="cb"><div class="ct">' |
| + '<div class="ct-title">'+TITULOS_L2[i]+'</div>' |
| + '<div class="ct-sub">'+SUBS_L2[i]+'</div></div>' |
| + '<div class="cp" id="l2_'+i+'"></div></div>'; |
| } |
| } |
| |
| function gsize(id) { |
| var el = document.getElementById(id); |
| if (!el) return [200,150]; |
| return [el.clientWidth||200, el.clientHeight||150]; |
| } |
| |
| var tickvals = Object.keys(MESES).map(Number); |
| var ticktext = Object.values(MESES); |
| |
| function fmtDot(v) { |
| var n = Math.round(v); |
| if (n===0) return '0'; |
| var s=n.toString(), r='', c=0; |
| for (var i=s.length-1; i>=0; i--) { |
| if (c>0 && c%3===0) r='.'+r; |
| r=s[i]+r; c++; |
| } |
| return r; |
| } |
| |
| function renderBarH(item, elId) { |
| var wh = gsize(elId); |
| var el = document.getElementById(elId); |
| var meses = item.meses_ex || []; |
| var data = []; |
| var yOrder = meses.map(function(m){ return MESES[m]; }).slice().reverse(); |
| var maxVal = 0; |
| item.traces.forEach(function(t){ t.y.forEach(function(v){ if(v>maxVal) maxVal=v; }); }); |
| item.traces.forEach(function(t) { |
| var yV = t.y.slice().reverse(); |
| data.push({ |
| type:'bar', orientation:'h', name:t.ano, |
| x:yV, y:yOrder, |
| text:yV.map(function(){ return ''; }), |
| marker:{color:t.cor, line:{color:'white',width:1}}, |
| hovertemplate:'<b>'+t.ano+' - %{y}</b><br>%{x:,.0f}<extra></extra>' |
| }); |
| }); |
| var annotations = []; |
| var accum = yOrder.map(function(){ return 0; }); |
| item.traces.forEach(function(t) { |
| var yV = t.y.slice().reverse(); |
| yV.forEach(function(v, i) { |
| if (!v) { accum[i]+=(v||0); return; } |
| var xStart = accum[i]; |
| var inside = v >= maxVal*0.10; |
| annotations.push({ |
| x: inside ? xStart+v/2 : xStart+v+maxVal*0.015, |
| y: yOrder[i], |
| xref:'x', yref:'y', showarrow:false, |
| xanchor: inside?'center':'left', |
| yanchor:'middle', |
| text:'<b>'+fmtDot(v)+'</b>', |
| font:{size:11,family:'DM Sans',color:inside?'white':'#1e3a5f'} |
| }); |
| accum[i] += v; |
| }); |
| }); |
| var layout = { |
| width:wh[0], height:wh[1], autosize:true, barmode:'stack', |
| template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:11}, |
| margin:{l:70,r:56,t:8,b:18}, |
| plot_bgcolor:'rgba(248,250,252,0.8)', paper_bgcolor:'white', |
| bargap:0.28, annotations:annotations, |
| xaxis:{tickfont:{size:9,family:'DM Sans',color:'#9ca3af'}, |
| showgrid:true, gridcolor:'rgba(200,200,200,0.25)', |
| rangemode:'tozero', zeroline:false, range:[0,maxVal*1.25]}, |
| yaxis:{tickfont:{size:11,family:'DM Sans',color:'#374151'}, |
| showgrid:false, categoryorder:'array', categoryarray:yOrder, ticksuffix:' '}, |
| hoverlabel:{bgcolor:'white',font_size:12,font_family:'DM Sans'} |
| }; |
| if (!inited[elId]) { |
| Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true}); |
| inited[elId] = true; |
| } else { Plotly.react(el, data, layout); } |
| roundBars(el, 8, 'h'); |
| } |
| |
| function renderBarV(item, elId) { |
| var wh = gsize(elId); |
| var el = document.getElementById(elId); |
| var meses = item.meses_ex || []; |
| var data = []; |
| var maxByMes = {}; |
| meses.forEach(function(m,mi) { |
| var s=0; item.traces.forEach(function(t){ s+=t.y[mi]||0; }); maxByMes[mi]=s; |
| }); |
| var globalMax = Math.max.apply(null, meses.map(function(_,mi){ return maxByMes[mi]; })); |
| var annotations = []; |
| meses.forEach(function(m,mi) { |
| var tot = maxByMes[mi]; |
| if (!tot) return; |
| annotations.push({x:MESES[m],y:tot,xref:'x',yref:'y',showarrow:false, |
| yanchor:'bottom',xanchor:'center',yshift:5, |
| text:'<b>'+fmtDot(tot)+'</b>',font:{size:11,family:'DM Sans',color:'#1e3a5f'}}); |
| }); |
| var accumV = meses.map(function(){ return 0; }); |
| item.traces.forEach(function(t) { |
| var segAnnots = []; |
| t.y.forEach(function(v,mi) { |
| if (v>0) { |
| var yBase = accumV[mi]; |
| if (v >= globalMax*0.08) { |
| segAnnots.push({x:MESES[meses[mi]],y:yBase+v/2,xref:'x',yref:'y',showarrow:false, |
| xanchor:'center',yanchor:'middle', |
| text:'<b>'+fmtDot(v)+'</b>',font:{size:10,family:'DM Sans',color:'white'}}); |
| } |
| accumV[mi] += v; |
| } else { accumV[mi]+=(v||0); } |
| }); |
| segAnnots.forEach(function(a){ annotations.push(a); }); |
| data.push({type:'bar',name:t.ano, |
| x:meses.map(function(m){ return MESES[m]; }), y:t.y, |
| text:t.y.map(function(){ return ''; }), |
| marker:{color:t.cor, line:{color:'white',width:1}}, |
| hovertemplate:'<b>'+t.ano+' - %{x}</b><br>%{y:,.0f}<extra></extra>'}); |
| }); |
| var layout = { |
| width:wh[0], height:wh[1], autosize:true, barmode:'stack', |
| template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:11}, |
| margin:{l:34,r:16,t:30,b:32}, |
| plot_bgcolor:'rgba(248,250,252,0.8)', paper_bgcolor:'white', |
| bargap:0.28, annotations:annotations, |
| xaxis:{tickfont:{size:11,family:'DM Sans',color:'#374151'},showgrid:false,tickangle:0}, |
| yaxis:{tickfont:{size:9,family:'DM Sans',color:'#9ca3af'},showgrid:true, |
| gridcolor:'rgba(200,200,200,0.25)',zeroline:false,range:[0,globalMax*1.20]}, |
| hoverlabel:{bgcolor:'white',font_size:12,font_family:'DM Sans'} |
| }; |
| if (!inited[elId]) { |
| Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true}); |
| inited[elId] = true; |
| } else { Plotly.react(el, data, layout); } |
| roundBars(el, 8, 'v'); |
| } |
| |
| function renderLine(item, elId) { |
| var wh = gsize(elId); |
| var el = document.getElementById(elId); |
| var data = [], annotations = [], yshifts = [18,32,46,60]; |
| item.traces.forEach(function(t, i) { |
| data.push({ |
| type:'scatter', mode:'lines+markers', |
| x:t.meses, y:t.y, |
| line:{width:3, color:t.cor, shape:'spline'}, |
| marker:{size:8, color:t.cor, line:{width:2,color:'white'}}, |
| fill: i>0?'tonexty':null, fillcolor:t.fill_rgba, |
| cliponaxis:false, showlegend:false, name:t.ano, |
| hovertemplate:'<b>'+t.ano+' - %{customdata}</b><br>%{y:,.0f}<extra></extra>', |
| customdata:t.meses.map(function(m){ return MESES[m]; }) |
| }); |
| var shift = yshifts[i % yshifts.length]; |
| t.meses.forEach(function(mx, j) { |
| if (!t.text[j]) return; |
| annotations.push({x:mx,y:t.y[j],xref:'x',yref:'y',text:t.text[j], |
| showarrow:false,yshift:shift, |
| font:{size:11,family:'DM Sans',color:'#1a1a1a'},bgcolor:'rgba(0,0,0,0)'}); |
| }); |
| }); |
| var layout = { |
| autosize:true, |
| hovermode:'x unified', template:'plotly_white', |
| font:{family:'DM Sans',size:9}, showlegend:false, |
| margin:{l:38,r:18,t:30,b:38}, |
| plot_bgcolor:'rgba(248,249,250,0.5)', annotations:annotations, |
| xaxis:{tickmode:'array',tickvals:tickvals,ticktext:ticktext,tickangle:0, |
| tickfont:{size:9,family:'DM Sans'},showgrid:true, |
| gridcolor:'rgba(200,200,200,0.2)',ticklabelstandoff:8}, |
| yaxis:{tickfont:{size:9,family:'DM Sans'},rangemode:'tozero', |
| showgrid:true,gridcolor:'rgba(200,200,200,0.2)'}, |
| hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans'} |
| }; |
| if (!inited[elId]) { |
| Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true}); |
| inited[elId] = true; |
| } else { Plotly.react(el, data, layout); } |
| } |
| |
| function renderDonut(item, elId) { |
| if (!item.valores.length) return; |
| var wh = gsize(elId); |
| var el = document.getElementById(elId); |
| var total = item.valores.reduce(function(s,v){ return s+v; }, 0); |
| var labels = item.labels.map(function(l,i) { |
| var pct = total>0 ? (item.valores[i]/total*100).toFixed(1) : '0.0'; |
| return l+': '+fmtDot(item.valores[i])+' ('+pct+'%)'; |
| }); |
| var textF = item.valores.map(function(v){ return total>0?(v/total*100).toFixed(1)+'%':''; }); |
| var data = [{ |
| type:'pie', labels:labels, values:item.valores, |
| marker:{colors:item.cores, line:{color:'white',width:3}}, |
| hole:0.4, text:textF, textinfo:'text', |
| textfont:{size:12,family:'Sora'}, insidetextorientation:'radial', |
| hovertemplate:'<b>%{label}</b><extra></extra>', |
| pull:item.valores.map(function(v){ return v===Math.max.apply(null,item.valores)?0.03:0; }), |
| rotation:90, direction:'clockwise', sort:false |
| }]; |
| var layout = { |
| autosize:true, |
| template:'plotly_white', font:{family:'DM Sans',size:9}, showlegend:true, |
| legend:{orientation:'v',yanchor:'middle',y:0.5,xanchor:'left',x:1.02,font:{size:8,family:'DM Sans',color:'#333'}}, |
| margin:{l:10,r:130,t:10,b:10}, |
| hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans'} |
| }; |
| if (!inited[elId]) { |
| Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true}); |
| inited[elId] = true; |
| } else { Plotly.react(el, data, layout); } |
| } |
| |
| function renderLineSis(item, elId) { |
| if (!item.valores.length) return; |
| var wh = gsize(elId); |
| var el = document.getElementById(elId); |
| var total = item.valores.reduce(function(s,v){ return s+v; }, 0); |
| var xVals = item.labels.map(function(_,i){ return i; }); |
| var texts = item.valores.map(function(v) { |
| var pct = total>0 ? (v/total*100).toFixed(1) : '0.0'; |
| return v>0 ? '<b>'+fmtDot(v)+'</b><br>'+pct+'%' : ''; |
| }); |
| var cor = item.cores[0] || '#0056A3'; |
| var r_c = parseInt(cor.slice(1,3),16); |
| var g_c = parseInt(cor.slice(3,5),16); |
| var b_c = parseInt(cor.slice(5,7),16); |
| var fill = 'rgba('+r_c+','+g_c+','+b_c+',0.2)'; |
| var data = [{ |
| type:'scatter', mode:'lines+markers+text', |
| x:xVals, y:item.valores, |
| text:texts, textposition:'top center', |
| textfont:{size:11,family:'DM Sans',color:'#1a1a1a'}, |
| line:{width:4, color:cor, shape:'spline'}, |
| marker:{size:14, color:item.cores, symbol:'circle'}, |
| fill:'tozeroy', fillcolor:fill, |
| hovertemplate:'<b>%{customdata}</b><br>%{y:,.0f}<extra></extra>', |
| customdata:item.labels, showlegend:false, cliponaxis:false |
| }]; |
| var layout = { |
| autosize:true, |
| template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:9}, |
| margin:{l:10,r:10,t:30,b:72}, |
| plot_bgcolor:'rgba(248,249,250,0.5)', paper_bgcolor:'white', |
| xaxis:{tickmode:'array',tickvals:xVals,ticktext:item.labels, |
| tickfont:{size:8,family:'DM Sans',color:'#333'},tickangle:-35, |
| showgrid:false,zeroline:false,showline:false,ticks:'',automargin:true}, |
| yaxis:{showgrid:true,gridcolor:'rgba(200,200,200,0.35)',gridwidth:1, |
| zeroline:false,showline:false,showticklabels:false,ticks:'', |
| range:[0,Math.max.apply(null,item.valores)*1.6]}, |
| hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans'} |
| }; |
| if (!inited[elId]) { |
| Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true}); |
| inited[elId] = true; |
| } else { Plotly.react(el, data, layout); } |
| } |
| |
| // ── FIXED: added roundBars call to render rounded tops on bar chart ── |
| function renderBarVSis(item, elId) { |
| if (!item.valores.length) return; |
| var wh = gsize(elId); |
| var el = document.getElementById(elId); |
| var total = item.valores.reduce(function(s,v){ return s+v; }, 0); |
| var maxVal = Math.max.apply(null, item.valores); |
| var annotations = item.labels.map(function(l,i) { |
| var v = item.valores[i]; |
| var pct = total>0 ? (v/total*100).toFixed(1) : '0.0'; |
| return {x:l, y:v, xref:'x', yref:'y', showarrow:false, |
| yanchor:'bottom', xanchor:'center', yshift:4, |
| text:'<b>'+fmtDot(v)+'</b><br><span style="font-size:10px">'+pct+'%</span>', |
| font:{size:11,family:'DM Sans',color:'#1e3a5f'}}; |
| }); |
| var data = [{ |
| type:'bar', x:item.labels, y:item.valores, |
| text:item.valores.map(function(){ return ''; }), |
| marker:{color:item.cores, line:{color:'white',width:2}}, |
| hovertemplate:'<b>%{x}</b><br>%{y:,.0f}<extra></extra>', |
| width:0.6, showlegend:false |
| }]; |
| var layout = { |
| autosize:true, |
| template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:11}, |
| margin:{l:30,r:18,t:40,b:72}, |
| plot_bgcolor:'rgba(248,249,250,0.6)', paper_bgcolor:'white', |
| bargap:0.3, annotations:annotations, |
| xaxis:{tickfont:{size:9,family:'DM Sans',color:'#374151'},showgrid:false,tickangle:-30,automargin:true}, |
| yaxis:{tickfont:{size:9,family:'DM Sans'},showgrid:true, |
| gridcolor:'rgba(200,200,200,0.35)',range:[0,maxVal*1.35],showticklabels:false,zeroline:false}, |
| hoverlabel:{bgcolor:'white',font_size:12,font_family:'DM Sans'} |
| }; |
| if (!inited[elId]) { |
| Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true}); |
| inited[elId] = true; |
| } else { Plotly.react(el, data, layout); } |
| // ── CORREÇÃO: arredondar o topo das barras ── |
| roundBars(el, 8, 'v'); |
| } |
| |
| function roundBars(boxEl, r, orientation) { |
| setTimeout(function() { |
| boxEl.querySelectorAll('.bars path').forEach(function(p) { |
| var d = p.getAttribute('d'); |
| if (!d) return; |
| var tok = d.match(/[A-Za-z][^A-Za-z]*/g) || []; |
| var cx=0, cy=0, aX=[], aY=[]; |
| tok.forEach(function(t) { |
| var cmd = t[0]; |
| var pts = t.slice(1).trim().split(/[\\s,]+/).map(Number).filter(function(n){ return !isNaN(n); }); |
| if (cmd==='M'||cmd==='L') { aX.push(pts[0]); aY.push(pts[1]); cx=pts[0]; cy=pts[1]; } |
| else if (cmd==='H') { aX.push(pts[0]); aY.push(cy); cx=pts[0]; } |
| else if (cmd==='V') { aX.push(cx); aY.push(pts[0]); cy=pts[0]; } |
| }); |
| if (aX.length < 2) return; |
| var x0=Math.min.apply(null,aX), x1=Math.max.apply(null,aX); |
| var y0=Math.min.apply(null,aY), y1=Math.max.apply(null,aY); |
| var w=x1-x0, h=y1-y0; |
| if (w<=0 || h<=0) return; |
| var rr = Math.min(r, w/2, h/2); |
| var nd; |
| if (orientation === 'h') { |
| nd = 'M '+x0+','+y0 |
| + ' H '+(x1-rr)+' Q '+x1+','+y0+' '+x1+','+(y0+rr) |
| + ' V '+(y1-rr)+' Q '+x1+','+y1+' '+(x1-rr)+','+y1 |
| + ' H '+x0+' V '+y0+' Z'; |
| } else { |
| nd = 'M '+x0+','+y1 |
| + ' H '+x1 |
| + ' V '+(y0+rr) |
| + ' Q '+x1+','+y0+' '+(x1-rr)+','+y0 |
| + ' H '+(x0+rr)+' Q '+x0+','+y0+' '+x0+','+(y0+rr) |
| + ' V '+y1+' Z'; |
| } |
| p.setAttribute('d', nd); |
| }); |
| }, 80); |
| } |
| |
| function renderFrame(f) { |
| CARDS_CFG.forEach(function(c) { |
| var cd = f.cards[c.col]; |
| if (cd) document.getElementById('mv_'+c.col).textContent = cd.valor+cd.sufixo; |
| }); |
| f.linha1.forEach(function(item, i) { |
| if (item.tipo==='barra_h') renderBarH(item, 'l1_'+i); |
| else if (item.tipo==='barra_v') renderBarV(item, 'l1_'+i); |
| else if (item.tipo==='linha') renderLine(item, 'l1_'+i); |
| }); |
| f.linha2.forEach(function(item, i) { |
| if (item.tipo==='donut') renderDonut(item, 'l2_'+i); |
| else if (item.tipo==='linha_sis') renderLineSis(item, 'l2_'+i); |
| else if (item.tipo==='barv_sis') renderBarVSis(item, 'l2_'+i); |
| }); |
| } |
| |
| function stepFrame() { |
| renderFrame(FRAMES[frameIdx]); |
| frameIdx++; |
| if (frameIdx >= FRAMES.length) { |
| clearTimeout(animTimer); |
| cdownTimer = setTimeout(function(){ frameIdx=0; startAnimation(); }, PAUSE_MS); |
| } else { |
| animTimer = setTimeout(stepFrame, FRAME_MS); |
| } |
| } |
| |
| function startAnimation() { |
| clearTimeout(animTimer); |
| clearTimeout(cdownTimer); |
| animTimer = setTimeout(stepFrame, FRAME_MS); |
| } |
| |
| var _rt; |
| window.addEventListener('resize', function() { |
| clearTimeout(_rt); |
| _rt = setTimeout(function() { |
| document.querySelectorAll('.cp').forEach(function(box) { |
| if (box.data) Plotly.relayout(box, {width:box.clientWidth, height:box.clientHeight}); |
| }); |
| }, 200); |
| }); |
| |
| window.addEventListener('load', function() { |
| buildDOM(); |
| setTimeout(function(){ frameIdx=0; startAnimation(); }, 400); |
| }); |
| </script> |
| </body> |
| </html>""" |
|
|
| html = (html |
| .replace('__FRAMES__', frames_json) |
| .replace('__MESES__', meses_json) |
| .replace('__TITULOS_L1__', json.dumps(TITULOS_L1)) |
| .replace('__TITULOS_L2__', json.dumps(TITULOS_L2)) |
| .replace('__SUBS_L1__', json.dumps(subs_l1)) |
| .replace('__SUBS_L2__', json.dumps(subs_l2)) |
| ) |
|
|
| components.html(html, height=8000, scrolling=True) |
|
|
| else: |
| components.html("""<!DOCTYPE html><html><body style="display:flex;align-items:center; |
| justify-content:center;height:100dvh;font-family:sans-serif;color:#003a70;background:#f5f7fa;"> |
| <div style="text-align:center"><div style="font-size:2em">📊</div> |
| <p>Aguardando dados... Recarregue a página.</p></div></body></html>""", |
| height=4000, scrolling=False) |
|
|
| time.sleep(60) |
| st.rerun() |