Spaces:
Sleeping
Sleeping
| # app.py | |
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| # ============================================================= | |
| # CONFIGURACIÓN BÁSICA | |
| # ============================================================= | |
| CSV_PATH = "DEP.csv" | |
| COL_SX = "SEXO" | |
| COL_BREED = "CRIADOR" | |
| COL_ID = "NUMEROHATO" | |
| COL_REG = "REGISTRO" | |
| COL_MGT = "MGT" | |
| # Métricas (ajusta si tu CSV difiere) | |
| METRIC_MAP = { | |
| "Peso al nacimiento": "DEPN", | |
| "Peso al destete (205d)": "DEPP205", | |
| "Leche": "DEPLECHE", | |
| "Materno total": "DEPMATERNOTOTAL", | |
| "Peso al año (365d)": "DEPP365", | |
| "CE al año": "DEPCEA", | |
| "Peso a 18 meses (550d)": "DEPP550", | |
| "CE a 18 meses (550d)": "DEPCE550", | |
| "Mérito genético total": "MGT", | |
| } | |
| EX_MAP = { | |
| "Peso al nacimiento": "EXPN", | |
| "Peso al destete (205d)": "EXP205", | |
| "Leche": "EXLECHE", | |
| "Materno total": None, | |
| "Peso al año (365d)": "EXP365", | |
| "CE al año": "EXCEA", | |
| "Peso a 18 meses (550d)": "EXP550", | |
| "CE a 18 meses (550d)": "EXCE550", | |
| "Mérito genético total": None, | |
| } | |
| RADAR_ORDER = list(METRIC_MAP.keys()) | |
| BASE_COLS = [COL_ID, COL_REG, COL_SX, COL_BREED, COL_MGT] | |
| TABLE_METRIC_COLS = [METRIC_MAP[k] for k in RADAR_ORDER if METRIC_MAP[k] != COL_MGT] + [COL_MGT] | |
| DISPLAY_COLS = BASE_COLS + [c for c in TABLE_METRIC_COLS if c not in BASE_COLS] | |
| # Paleta | |
| COLOR_A = "#46973d" | |
| COLOR_B = "#ddb516" | |
| NEUTRAL = "#dcdcdc" | |
| # ============================================================= | |
| # UTILIDADES DE NORMALIZACIÓN | |
| # ============================================================= | |
| def normalize_name(s: str) -> str: | |
| if s is None or (isinstance(s, float) and pd.isna(s)): return "" | |
| s = str(s).strip() | |
| while s.endswith("."): | |
| s = s[:-1].rstrip() | |
| s = " ".join(s.split()) | |
| return s | |
| # ============================================================= | |
| # 🔐 CONTRASEÑAS (EDITABLE EN UN SOLO LUGAR) | |
| # ------------------------------------------------------------- | |
| # IMPORTANTE: Las llaves deben estar ya "normalizadas" como arriba. | |
| PASSWORDS = { | |
| normalize_name("BEECHE_BRAHMANS_S_A"): "BEECHE2025", | |
| normalize_name("GANADERIA_COLONO_REAL_S_A"): "COLONOREAL2025", | |
| normalize_name("ALVARO_CLACHAR_B_Y_O_HDA_EL_REAL_S_A"): "CLACHAR2025", | |
| normalize_name("HDA_EL_ESCUDO_ANTONIO_RODRIGUEZ_S_A"): "ESCUDO2025", | |
| normalize_name("ANGUIZOLA_M_SERGIO"): "ANGUIZOLA2025", | |
| normalize_name("CARLOS_LEE"): "LEE2025", | |
| normalize_name("GANADERA_GUACHIPELIN_DEL_SUR_S_A"): "GUACHIPELIN2025", | |
| normalize_name("GANADERA_KARLA_MARY_S_A"): "KARLAMARY2025", | |
| normalize_name("GANADERA_SAVAL_S_A"): "SAVAL2025", | |
| normalize_name("GANADERIA_JL_MORALES_FINCA_EL_ROSARIO_S"): "MORALES2025", | |
| normalize_name("GRUPO_MAG_NICARAGUA"): "MAGNICARAGUA2025", | |
| normalize_name("HACIENDA_PANDORA_S_A"): "PANDORA2025", | |
| normalize_name("HERITAGE_CATTLE_COMPANY_HUNGERFORD"): "HERITAGE2025", | |
| normalize_name("HERMACOR_S_A"): "HERMACOR2025", | |
| normalize_name("HK_CATTLE"): "HKCATTLE2025", | |
| normalize_name("JESSICA_SMITH"): "SMITH2025", | |
| normalize_name("JIM_S_WILLIAMS"): "WILLIAMS2025", | |
| normalize_name("LA_PRADERA_DEL_NORTE_LTDA"): "PRADERA2025", | |
| normalize_name("LA_PRECIOSA_S_A"): "PRECIOSA2025", | |
| normalize_name("LESLIE_W_HUDGINS"): "HUDGINS2025", | |
| normalize_name("MEGAN_ELISE_CULLERS"): "CULLERS2025", | |
| normalize_name("PRODUCTOS_PEDREGAL_S_A"): "PEDREGAL2025", | |
| normalize_name("RAIMUNDO_RIOJAS_Y_O_REGINA_DE_R"): "RIOJASREGINA2025", | |
| normalize_name("RANCHO_SIGUACAN_EN_SEVILLA"): "SIGUACAN2025", | |
| normalize_name("SEMINOLE_S_A"): "SEMINOLE2025", | |
| normalize_name("V8_RANCH"): "V8RANCH2025", | |
| normalize_name("ARIAS_Z_JOSE_RAMON"): "ARIAS2025", | |
| normalize_name("ECOS_DEL_PORVENIR_S_A"): "ECOSDELPORVENIR2025", | |
| normalize_name("GANADERIA_SAN_CRIST_BAL_S_A"): "SANCRISTOBAL2025", | |
| normalize_name("INTA_EJN"): "INTA", | |
| normalize_name("GANADERA_HURTADO_LTDA"): "HURTADO2025", | |
| normalize_name("FRANZ_W_HEINSOHN_MONTERO"): "HEINSOHNMONTERO2025", | |
| normalize_name("HACIENDA_SOLIMAR_LTDA"): "SOLIMAR2025", | |
| normalize_name("ESC_DE_AGRICULTURA_REGION_TROP_HUMEDA"): "AGRICULTURATROPICAL2025", | |
| normalize_name("AGRICOLA_EL_CANTARO_S_A"): "CANTARO2025", | |
| normalize_name("MURILLO_KOPPER_TULIO"): "MURILLOKOPPER2025", | |
| normalize_name("AGROPECUARIA_EL_DORADO_S_A"): "ELDORADO2025", | |
| normalize_name("CIA_GANADERA_GUACIMAL_S_A"): "GUACIMAL2025", | |
| normalize_name("JOSE_E_BONILLA_ESQUIVEL"): "JOSEBONILLAESQUIVEL2025", | |
| normalize_name("SHARON_S_DE_WOLF"): "WOLF2025", | |
| normalize_name("PATRICIO_PITTI"): "PITTI2025", | |
| normalize_name("TULLOCH_ESTATE_LTD"): "TULLOCHESTATE2025", | |
| normalize_name("GANADERIA_CRUZ_S_A"): "CRUZ2025", | |
| normalize_name("HACIENDA_CHOROTEGA_S_A"): "CHOROTEGA2025", | |
| normalize_name("GANADERIA_HERACOS_GHA_S_A"): "HERACOS2025", | |
| normalize_name("GANADERIA_EL_ROSARIO_Y_O_MARLA_ARGUELLO"): "ELROSARIOARGUELLO2025", | |
| normalize_name("GP_BRAHMANS"): "GPBRAHMANS2025", | |
| normalize_name("INSTITUTO_TECNOLOGICO_DE_COSTA_RICA"): "ITCR2025", | |
| normalize_name("HACIENDA_EL_RETIRO_LTDA"): "ELRETIRO2025", | |
| normalize_name("INTA_LD"): "INTA", | |
| normalize_name("DESCONOCIDO"): "DESCONOCIDO2025", | |
| normalize_name("ESCUELA_CENTROAMERICANA_DE_GANADERIA"): "ESCUELAGANADERIA2025", | |
| normalize_name("J_D_HUDGINS_FORGASON_DIV"): "JDHUDGINS2025", | |
| normalize_name("SIQUIARES_S_A"): "SIQUIARES2025", | |
| normalize_name("UNITED_STATES_SUGAR_CORP"): "USSUGAR2025", | |
| normalize_name("SLOAN_WILLIAMS"): "SLOANWILLIAMS2025", | |
| normalize_name("MONTANA_S_A"): "CANAGUA", | |
| normalize_name("ANIMAR_LTDA"): "ANIMAR2025", | |
| normalize_name("JOSE_CAMPOS_QUESADA"): "JOSECAMPOSQUESADA2025", | |
| normalize_name("HACIENDA_SANTA_PAULA_S_A"): "SANTAPAULA2025", | |
| normalize_name("GANADERA_ABR_S_A"): "ABR2025", | |
| normalize_name("GANADERA_MONTERREY_S_A"): "MONTERREY2025", | |
| normalize_name("GANADERA_XIRINACHS_BATALLA_DE_C_R_S_A"): "XIRINACHS2025", | |
| } | |
| def password_for_criador(criador_norm: str) -> str: | |
| return PASSWORDS.get(criador_norm or "", "") | |
| # ============================================================= | |
| # UTILIDADES | |
| # ============================================================= | |
| def _coerce_numeric(df, cols): | |
| for c in cols: | |
| if c in df.columns: | |
| df[c] = pd.to_numeric(df[c], errors="coerce") | |
| return df | |
| def _normalize_minmax(x, gmin, gmax): | |
| s = x if isinstance(x, pd.Series) else pd.Series([x]) | |
| min_s = pd.Series([gmin] * len(s), index=s.index) if np.isscalar(gmin) else gmin.reindex(s.index, fill_value=gmin.iloc[0] if len(gmin) else 0) | |
| max_s = pd.Series([gmax] * len(s), index=s.index) if np.isscalar(gmax) else gmax.reindex(s.index, fill_value=gmax.iloc[0] if len(gmax) else 1) | |
| rng = (max_s - min_s).replace(0, 1) | |
| return ((s - min_s) / rng).fillna(0.0).clip(0, 1) | |
| def _hex_to_rgb(h): h = h.lstrip("#"); return tuple(int(h[i:i+2], 16) for i in (0,2,4)) | |
| def _rgb_to_hex(rgb): return "#%02x%02x%02x" % rgb | |
| def _lerp_color(a_hex, b_hex, t): | |
| a = _hex_to_rgb(a_hex); b = _hex_to_rgb(b_hex) | |
| c = tuple(int(round(a[i] + (b[i]-a[i]) * float(t))) for i in range(3)) | |
| return _rgb_to_hex(c) | |
| def _coerce_ex_to_unit(ex): | |
| if ex is None: return None | |
| try: val = float(ex) | |
| except: return None | |
| t = val if val <= 1.5 else val/100.0 | |
| if np.isnan(t): return None | |
| return max(0.0, min(1.0, t)) | |
| def _build_radar_with_accuracy(values_dict, acc_dict, title): | |
| labels = list(values_dict.keys()) | |
| values = [values_dict[k] for k in labels] | |
| theta = labels + [labels[0]] | |
| r_vals = values + [values[0]] | |
| bar_colors, hovertexts = [], [] | |
| for lab in labels: | |
| t = _coerce_ex_to_unit(acc_dict.get(lab, None)) | |
| if t is None: | |
| bar_colors.append(NEUTRAL) | |
| hovertexts.append(f"{lab}<br>Exactitud: —") | |
| else: | |
| # degradado por exactitud | |
| bar_colors.append(_lerp_color(COLOR_A, COLOR_B, t)) | |
| hovertexts.append(f"{lab}<br>Exactitud: {int(round(t*100))}%") | |
| fig = go.Figure() | |
| fig.add_trace(go.Barpolar( | |
| r=[1.0]*len(labels), | |
| theta=labels, | |
| marker=dict(color=bar_colors, line=dict(color="white", width=1)), | |
| opacity=0.6, | |
| name="Exactitud (EX)", | |
| hovertext=hovertexts, | |
| hovertemplate="%{hovertext}<extra></extra>", | |
| )) | |
| fig.add_trace(go.Scatterpolar( | |
| r=r_vals, theta=theta, mode="lines+markers", fill="toself", | |
| name="Valor normalizado", | |
| line=dict(color=COLOR_A, width=3), | |
| marker=dict(size=6, color=COLOR_B), | |
| hovertemplate="<b>%{theta}</b><br>Valor: %{r:.2f}<extra></extra>", | |
| )) | |
| fig.update_layout( | |
| title=title, showlegend=True, paper_bgcolor="white", | |
| polar=dict( | |
| bgcolor="white", | |
| radialaxis=dict(visible=True, range=[0,1], gridcolor="#eeeeee", linecolor=COLOR_A), | |
| angularaxis=dict(gridcolor="#f2f2f2", linecolor=COLOR_A), | |
| ), | |
| margin=dict(l=10, r=10, t=60, b=10), | |
| legend=dict(orientation="h", yanchor="bottom", y=1.05, xanchor="center", x=0.5), | |
| ) | |
| return fig | |
| # ============================================================= | |
| # CARGA DE DATOS | |
| # ============================================================= | |
| def load_data(): | |
| df = pd.read_csv(CSV_PATH) | |
| if COL_SX in df.columns: | |
| df[COL_SX] = df[COL_SX].astype(str).str.upper().str.strip() | |
| df[COL_SX] = df[COL_SX].replace({"HEMBRA":"H","MACHO":"M"}) | |
| if COL_BREED in df.columns: | |
| df[COL_BREED] = df[COL_BREED].astype(str) | |
| df["CRIADOR_NORM"] = df[COL_BREED].map(normalize_name) | |
| df = _coerce_numeric(df, list(set(TABLE_METRIC_COLS + [COL_MGT]))) | |
| ex_cols = [c for c in EX_MAP.values() if c] | |
| df = _coerce_numeric(df, ex_cols) | |
| breeders_all = sorted([b for b in df["CRIADOR_NORM"].dropna().unique().tolist() if b not in ["", "nan"]]) | |
| metric_cols_present = [METRIC_MAP[k] for k in RADAR_ORDER if METRIC_MAP[k] in df.columns] | |
| global_mins = df[metric_cols_present].min(numeric_only=True) | |
| global_maxs = df[metric_cols_present].max(numeric_only=True) | |
| return df, breeders_all, global_mins, global_maxs | |
| # ============================================================= | |
| # CÁLCULOS | |
| # ============================================================= | |
| def _promedios_y_exactitud(df_subset, global_mins, global_maxs): | |
| valores, ex_vals = {}, {} | |
| for label in RADAR_ORDER: | |
| dep_col = METRIC_MAP[label] | |
| ex_col = EX_MAP.get(label, None) | |
| if dep_col in df_subset.columns: | |
| prom_val = pd.to_numeric(df_subset[dep_col], errors="coerce").mean() | |
| # normalizar con min-max global | |
| nrm = (prom_val - global_mins.get(dep_col, 0)) / max((global_maxs.get(dep_col, 1) - global_mins.get(dep_col, 0)), 1e-9) | |
| valores[label] = float(np.clip(nrm, 0, 1)) if pd.notnull(prom_val) else 0.0 | |
| else: | |
| valores[label] = 0.0 | |
| if ex_col and ex_col in df_subset.columns: | |
| ex_prom = pd.to_numeric(df_subset[ex_col], errors="coerce").mean() | |
| ex_vals[label] = float(ex_prom) if pd.notnull(ex_prom) else None | |
| else: | |
| ex_vals[label] = None | |
| return valores, ex_vals | |
| def _valores_y_exactitud_fila(row, df_cols, global_mins, global_maxs): | |
| valores, ex_vals = {}, {} | |
| for label in RADAR_ORDER: | |
| dep_col = METRIC_MAP[label] | |
| ex_col = EX_MAP.get(label, None) | |
| if dep_col in df_cols: | |
| val = pd.to_numeric(row.get(dep_col, np.nan), errors="coerce") | |
| if pd.notnull(val): | |
| nrm = (val - global_mins.get(dep_col, 0)) / max((global_maxs.get(dep_col, 1) - global_mins.get(dep_col, 0)), 1e-9) | |
| valores[label] = float(np.clip(nrm, 0, 1)) | |
| else: | |
| valores[label] = 0.0 | |
| else: | |
| valores[label] = 0.0 | |
| if ex_col and ex_col in df_cols: | |
| exv = pd.to_numeric(row.get(ex_col, np.nan), errors="coerce") | |
| ex_vals[label] = float(exv) if pd.notnull(exv) else None | |
| else: | |
| ex_vals[label] = None | |
| return valores, ex_vals | |
| def top_hembras_por_criador(df, criador_norm, global_mins, global_maxs): | |
| dff = df.copy() | |
| if criador_norm and criador_norm != "—": | |
| dff = dff[dff["CRIADOR_NORM"] == criador_norm] | |
| dff = dff[dff[COL_SX] == "H"] | |
| dff = dff.sort_values(by=COL_MGT, ascending=False, na_position="last").head(30) | |
| cols_exist = [c for c in DISPLAY_COLS if c in dff.columns] | |
| tabla = dff[cols_exist].reset_index(drop=True) | |
| valores, ex_vals = _promedios_y_exactitud(dff, global_mins, global_maxs) | |
| titulo = f"Promedio Hembras • {criador_norm}" if criador_norm else "Promedio Hembras" | |
| fig = _build_radar_with_accuracy(valores, ex_vals, titulo) | |
| return tabla, fig | |
| def top_machos_por_metrica(df, metric_label, criador_norm): | |
| met_col = METRIC_MAP.get(metric_label, COL_MGT) | |
| dff = df.copy() | |
| if criador_norm and criador_norm != "—": | |
| dff = dff[dff["CRIADOR_NORM"] == criador_norm] | |
| dff = dff[dff[COL_SX] == "M"] | |
| dff = dff.sort_values(by=met_col, ascending=False, na_position="last").head(50) | |
| cols_exist = [c for c in DISPLAY_COLS if c in dff.columns] | |
| tabla = dff[cols_exist].reset_index(drop=True) | |
| return tabla | |
| def radar_individual_from_selection(evt: gr.SelectData, df_machos_current, global_mins, global_maxs): | |
| if df_machos_current is None or len(df_machos_current) == 0: | |
| return gr.update(value=None) | |
| try: | |
| row_idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index | |
| except Exception: | |
| return gr.update(value=None) | |
| if row_idx is None or row_idx >= len(df_machos_current): | |
| return gr.update(value=None) | |
| row = df_machos_current.iloc[int(row_idx)] | |
| valores, ex_vals = _valores_y_exactitud_fila(row, df_machos_current.columns, global_mins, global_maxs) | |
| ident = str(row.get(COL_ID, "")) or "(sin ID)" | |
| fig = _build_radar_with_accuracy(valores, ex_vals, f"Radar individuo • {ident}") | |
| return fig | |
| # ============================================================= | |
| # INTERFAZ | |
| # ============================================================= | |
| df_global, breeders_all, global_mins, global_maxs = load_data() | |
| # Núcleo visible por defecto (opcional) | |
| core_breeders = [] | |
| for name in ["INTA-EJN", "INTA-LOS DIAMANTES"]: | |
| core_breeders.append(normalize_name(name)) | |
| core_breeders = sorted(list(dict.fromkeys([b for b in core_breeders if b]))) | |
| init_criador = core_breeders[0] if core_breeders else "—" | |
| init_tabla_h, init_fig_h = top_hembras_por_criador(df_global, init_criador, global_mins, global_maxs) | |
| init_tabla_m = top_machos_por_metrica(df_global, "Mérito genético total", init_criador) | |
| def _auth_message(ok: bool, criador_norm: str) -> str: | |
| if not criador_norm or criador_norm == "—": | |
| return "Seleccione un CRIADOR." | |
| if ok: | |
| return f"✅ Acceso concedido para **{criador_norm}**." | |
| else: | |
| return f"🔒 Contraseña incorrecta para **{criador_norm}**." | |
| def _is_authorized(criador_norm: str, pwd_input: str) -> bool: | |
| if not criador_norm or criador_norm == "—": | |
| return False | |
| return (pwd_input or "") == password_for_criador(criador_norm) | |
| with gr.Blocks(title="Programa Nacional de Evaluación y Mejoramiento Genético") as demo: | |
| # CSS | |
| gr.HTML(f""" | |
| <style> | |
| :root {{ color-scheme: light; }} | |
| body, .gradio-container, .prose, .prose * {{ | |
| color: #DDB516 !important; | |
| background: #ffffff !important; | |
| }} | |
| h1, h2, h3, .prose h1, .prose h2, .prose h3 {{ | |
| color: #DDB516 !important; | |
| }} | |
| .gr-button, button {{ border-radius: 10px; }} | |
| .gr-input, .gr-textbox, .gr-dropdown {{ border-color: {COLOR_A}; }} | |
| .gradio-container a {{ color: {COLOR_B}; }} | |
| .notice {{ font-size:14px; margin-top:6px; opacity:.9; }} | |
| </style> | |
| """) | |
| # Encabezado solicitado | |
| gr.Markdown("# Programa Nacional de Evaluación y Mejoramiento Genético") | |
| gr.Markdown( | |
| "En el año 2002 da inicio el Proyecto de Evaluación Genética de Bovinos de Carne registrados en Costa Rica, " | |
| "con la captura de datos de pesos de hembras y machos al destete, al año y a 18 meses, así como la medición de " | |
| "la circunferencia escrotal en machos, en fincas de criadores de Bovinos de las razas cebuinas, principalmente " | |
| "Brahman y Nelore. Este proyecto nació como iniciativa de la Corporación Ganadera- CORFOGA en cooperación con la " | |
| "Escuela de Ciencias Agrarias de la Universidad Nacional de Costa Rica-UNA y con la colaboración del Instituto de " | |
| "Innovación y Transferencia en Tecnología Agropecuaria-INTA y la Asociación de Criadores de Ganado Cebú de Costa Rica –ASOCEBU. Si quiere probar puede visualizar Fincas del Inta con el codigo INTA" | |
| ) | |
| gr.Markdown("## Selección y acceso por **CRIADOR**") | |
| with gr.Row(): | |
| dd_criador = gr.Dropdown( | |
| choices=["—"] + breeders_all if breeders_all else ["—"], | |
| value=(["—"] + core_breeders)[1] if core_breeders else "—", | |
| label="CRIADOR (deduplicado)" | |
| ) | |
| tb_password = gr.Textbox( | |
| label="Contraseña de la finca seleccionada", | |
| placeholder="Ingrese la contraseña del CRIADOR", | |
| type="password", | |
| value="" | |
| ) | |
| auth_msg = gr.Markdown("", elem_classes=["notice"]) | |
| gr.Markdown("Filtra **hembras** y **machos** por **CRIADOR** (la finca seleccionada). " | |
| "Para ver los datos, ingrese la contraseña de la finca.") | |
| machos_state = gr.State(value=init_tabla_m) | |
| # ---- HEMBRAS ---- | |
| with gr.Group(): | |
| gr.Markdown("## Hembras por **CRIADOR** (Top 30 por MGT)") | |
| with gr.Row(): | |
| out_tabla_hembras = gr.Dataframe(value=init_tabla_h, label="Hembras (Top 30 por MGT)", interactive=False) | |
| out_radar_hembras = gr.Plot(value=init_fig_h, label="Telaraña: Promedios Hembras (anillo = EX)") | |
| # ---- MACHOS ---- | |
| with gr.Group(): | |
| gr.Markdown("## Machos (Top 50) por métrica — filtrados por el mismo **CRIADOR**") | |
| metrica_rank = gr.Dropdown( | |
| choices=list(METRIC_MAP.keys()), | |
| value="Mérito genético total", | |
| label="Métrica para rankear machos" | |
| ) | |
| out_tabla_machos = gr.Dataframe(value=init_tabla_m, label="Machos (Top 50)", interactive=False) | |
| out_radar_macho = gr.Plot(label="Telaraña: Individuo (anillo = EX)") | |
| # ========================================================= | |
| # CALLBACKS | |
| # ========================================================= | |
| def update_all(criador_norm, pwd, metric_label): | |
| ok = _is_authorized(criador_norm, pwd) | |
| msg = _auth_message(ok, criador_norm) | |
| if not ok: | |
| empty_df = pd.DataFrame(columns=[c for c in DISPLAY_COLS if c in df_global.columns]) | |
| return ( | |
| msg, | |
| empty_df, gr.update(value=None), | |
| empty_df, empty_df, gr.update(value=None) | |
| ) | |
| # Autorizado: actualizar hembras y machos filtrados por la misma finca | |
| tabla_h, fig_h = top_hembras_por_criador(df_global, criador_norm if criador_norm != "—" else None, global_mins, global_maxs) | |
| tabla_m = top_machos_por_metrica(df_global, metric_label, criador_norm if criador_norm != "—" else None) | |
| return ( | |
| msg, | |
| tabla_h, fig_h, | |
| tabla_m, tabla_m, gr.update() | |
| ) | |
| dd_criador.change( | |
| fn=update_all, | |
| inputs=[dd_criador, tb_password, metrica_rank], | |
| outputs=[auth_msg, out_tabla_hembras, out_radar_hembras, out_tabla_machos, machos_state, out_radar_macho] | |
| ) | |
| tb_password.change( | |
| fn=update_all, | |
| inputs=[dd_criador, tb_password, metrica_rank], | |
| outputs=[auth_msg, out_tabla_hembras, out_radar_hembras, out_tabla_machos, machos_state, out_radar_macho] | |
| ) | |
| metrica_rank.change( | |
| fn=update_all, | |
| inputs=[dd_criador, tb_password, metrica_rank], | |
| outputs=[auth_msg, out_tabla_hembras, out_radar_hembras, out_tabla_machos, machos_state, out_radar_macho] | |
| ) | |
| def on_select_macho(evt: gr.SelectData, df_machos_current): | |
| return radar_individual_from_selection(evt, df_machos_current, global_mins, global_maxs) | |
| out_tabla_machos.select( | |
| fn=on_select_macho, | |
| inputs=[machos_state], | |
| outputs=[out_radar_macho] | |
| ) | |
| gr.Markdown( | |
| f"<div style='text-align:center; font-size: 14px; margin-top: 12px; opacity:0.85;'>" | |
| f"Elaborado por <b>Departamento de Proyectos Corfoga</b> • Paleta " | |
| f"<span style='color:{COLOR_A}'>{COLOR_A}</span> / <span style='color:{COLOR_B}'>{COLOR_B}</span>" | |
| f"</div>" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |