Ir al contenido principal

Construyendo un Dashboard Moderno con Python y Tkinter

Construyendo un Dashboard Moderno con Python y Tkinter

La creación de interfaces gráficas de usuario (GUI) y dashboards de datos ha evolucionado significativamente. Antes de la llegada de frameworks basados en web como Gradio, Streamlit o Taipy, Tkinter era la principal, y casi única, opción para desarrollar este tipo de aplicaciones en Python.

En este artículo, se explorará la vigencia de Tkinter, demostrando su poder y relevancia para la creación de GUIs nativas de escritorio y dashboards de datos.

Tkinter se presenta como una opción ideal para desarrolladores que necesitan crear herramientas internas, utilidades sencillas o software educativo. Su principal ventaja reside en su simplicidad: no requiere servidores web complejos, conocimientos de JavaScript o dependencias pesadas.

¿Qué es Tkinter y por qué debería importarte?

Tkinter es el kit de herramientas GUI estándar e integrado en Python. Su nombre es un juego de palabras de "Tk Interface". Es un wrapper alrededor de Tcl/Tk, un kit de herramientas GUI robusto y multiplataforma que existe desde principios de la década de 1990.

Su mayor ventaja es su inclusión en la biblioteca estándar de Python. Si tienes Python instalado, tienes Tkinter. No hay comandos pip install que ejecutar, ni conflictos de dependencia de entornos virtuales que resolver. Funciona "out of the box" en Windows, macOS y Linux.

Ventajas de Tkinter

  • Simplicidad y Velocidad: Para aplicaciones de tamaño pequeño a mediano, Tkinter permite un desarrollo rápido. Se puede tener una ventana funcional con elementos interactivos en pocas líneas de código.
  • Ligero: Las aplicaciones Tkinter tienen una huella mínima. No requieren un navegador o un servidor web, haciéndolas ideales para utilidades sencillas que necesitan ejecutarse eficientemente en cualquier máquina.
  • Apariencia Nativa (hasta cierto punto): Si bien Tkinter clásico tiene una apariencia anticuada, el conjunto de widgets temáticos ttk proporciona acceso a controles más modernos y de aspecto nativo que coinciden mejor con el sistema operativo anfitrión.
  • Excelente para el Aprendizaje: Tkinter enseña los conceptos fundamentales de la programación orientada a eventos, el núcleo de todo desarrollo GUI. Entender cómo gestionar widgets, layouts y eventos de usuario en Tkinter proporciona una base sólida para aprender cualquier otro framework GUI.

Si bien construir aplicaciones complejas y estéticamente exigentes puede ser un desafío, para su propósito previsto —crear aplicaciones de escritorio funcionales e independientes— sobresale.

Para modernizar la apariencia de las GUIs de Tkinter, existen bibliotecas adicionales como ttkbootstrap, que añade widgets extras y un estilo inspirado en Bootstrap.

Conceptos Clave de una Aplicación Tkinter

Toda aplicación Tkinter se construye sobre pilares fundamentales. Comprender estos conceptos es esencial antes de crear cualquier cosa significativa.

1/ La Ventana Raíz

La ventana raíz es el contenedor principal de toda la aplicación. Es la ventana de nivel superior que tiene una barra de título, botones de minimizar, maximizar y cerrar. Se crea con una sola línea de código:

import tkinter as tk

root = tk.Tk()
root.title("Mi Primera App Tkinter")

root.mainloop()

Todo lo demás en la aplicación —botones, etiquetas, campos de entrada, etc.— vivirá dentro de esta ventana raíz.

2/ Widgets

Los widgets son los bloques de construcción de la GUI. Son los elementos que el usuario ve e interactúa. Algunos de los widgets más comunes incluyen:

  • Label: Muestra texto estático o imágenes.
  • Button: Un botón que se puede hacer clic y que puede desencadenar una función.
  • Entry: Un campo de entrada de texto de una sola línea.
  • Text: Un área de entrada y visualización de texto de varias líneas.
  • Frame: Un contenedor rectangular invisible usado para agrupar otros widgets. Es crucial para organizar layouts complejos.
  • Canvas: Un widget versátil para dibujar formas, crear gráficos o mostrar imágenes.
  • Checkbutton y Radiobutton: Para selecciones booleanas o de opción múltiple.

3/ Gestores de Geometría

Una vez que hayas creado los widgets, necesitas indicarle a Tkinter dónde colocarlos dentro de la ventana. Este es el trabajo de los gestores de geometría. No se pueden mezclar y combinar diferentes gestores dentro del mismo contenedor padre (como una raíz o un Frame).

  • pack(): El gestor más simple. "Empaqueta" los widgets en la ventana, ya sea vertical u horizontalmente. Es rápido para layouts sencillos pero ofrece poco control preciso.
  • place(): El gestor más preciso. Permite especificar las coordenadas exactas de píxeles (x, y) y el tamaño (ancho, alto) de un widget. Generalmente, se debe evitar porque hace que la aplicación sea rígida y no responda al redimensionamiento de la ventana.
  • grid(): El gestor más potente y flexible, y el que se usará para este dashboard. Organiza los widgets en una estructura tipo tabla de filas y columnas, haciéndolo perfecto para crear layouts alineados y estructurados.

4/ El Bucle Principal

La línea root.mainloop() es la parte final y más crítica de cualquier aplicación Tkinter. Este método inicia el bucle de eventos. La aplicación entra en un estado de espera, escuchando las acciones del usuario como clics del ratón, pulsaciones de teclas o redimensionamiento de la ventana. Cuando ocurre un evento, Tkinter lo procesa (por ejemplo, llamando a una función ligada a un clic de botón) y luego regresa al bucle. La aplicación solo se cerrará cuando este bucle termine, usualmente al cerrar la ventana.

Ejemplo 2: Un Dashboard de Datos Moderno

Para este ejemplo, se creará un dashboard de datos utilizando un dataset de Kaggle llamado "CarsForSale". Este dataset tiene una licencia CC0: Dominio Público, lo que significa que puede ser usado libremente para la mayoría de los propósitos.

El dataset contiene detalles de ventas y rendimiento de aproximadamente 9300 modelos de coches diferentes de unos 40 fabricantes distintos, abarcando el período 2001–2022.

Desde una perspectiva de arquitectura, es importante señalar que la elección de Tkinter para este tipo de aplicación implica una compensación entre simplicidad y escalabilidad. Si bien Tkinter es ideal para prototipos rápidos y herramientas internas, para aplicaciones más complejas con necesidades de despliegue web y acceso concurrente, sería más adecuado considerar frameworks como Flask o Django.

###############################################################################
#  USED-CAR MARKETPLACE DASHBOARD
#
#
###############################################################################
import tkinter as tk
import ttkbootstrap as tb
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import pandas as pd, numpy as np, re, sys
from pathlib import Path
from textwrap import shorten

# ─────────────────────────  CONFIG  ──────────────────────────
CSV_PATH = r"C:\Users\thoma\temp\carsforsale.csv"       
COLUMN_ALIASES = {
    "brand": "make", "manufacturer": "make", "carname": "model",
    "rating": "consumerrating", "safety": "reliabilityrating",
}
REQUIRED = {"make", "price"}                            
# ──────────────────────────────────────────────────────────────

class Dashboard:
    # ═══════════════════════════════════════════════════════════
    def __init__(self, root: tb.Window):
        self.root = root
        self.style = tb.Style("darkly")
        self._make_spinbox_style()
        self.clr = self.style.colors
        
        self.current_analysis_plot_func = None 
        
        self._load_data()
        self._build_gui()
        self._apply_filters()

    # ─────────── spin-box style (white arrows) ────────────────
    def _make_spinbox_style(self):
        try:
            self.style.configure("White.TSpinbox",
                                 arrowcolor="white",
                                 arrowsize=12)
            self.style.map("White.TSpinbox",
                           arrowcolor=[("disabled", "white"),
                                       ("active",   "white"),
                                       ("pressed",  "white")])
        except tk.TclError:
            pass

    # ───────────────────── DATA LOAD ───────────────────────────
    def _load_data(self):
        csv = Path(CSV_PATH)
        if not csv.exists():
            tb.dialogs.Messagebox.show_error("CSV not found", str(csv))
            sys.exit()

        df = pd.read_csv(csv, encoding="utf-8-sig", skipinitialspace=True)
        df.columns = [
            COLUMN_ALIASES.get(
                re.sub(r"[^0-9a-z]", "", c.lower().replace("ufeff", "")),
                c.lower()
            )
            for c in df.columns
        ]
        if "year" not in df.columns:
            for col in df.columns:
                nums = pd.to_numeric(df[col], errors="coerce")
                if nums.dropna().between(1900, 2035).all():
                    df.rename(columns={col: "year"}, inplace=True)
                    break
        for col in ("price", "minmpg", "maxmpg",
                    "year", "mileage", "consumerrating"):
            if col in df.columns:
                df[col] = pd.to_numeric(
                    df[col].astype(str)
                          .str.replace(r"[^\d.]", "", regex=True),
                    errors="coerce"
                )
        if any(c not in df.columns for c in REQUIRED):
            tb.dialogs.Messagebox.show_error(
                "Bad CSV", "Missing required columns."
            )
            sys.exit()
        self.df = df.dropna(subset=["make", "price"])

    # ───────────────────── GUI BUILD ───────────────────────────
    def _build_gui(self):
        header = tb.Frame(self.root, width=600, height=60, bootstyle="dark")
        header.pack_propagate(False)
        header.pack(side="top", anchor="w", padx=8, pady=(4, 2))
        tb.Label(header, text="🚗  USED-CAR DASHBOARD",
                 font=("Segoe UI", 16, "bold"), anchor="w")\
          .pack(fill="both", padx=8, pady=4)

        self.nb = tb.Notebook(self.root); self.nb.pack(fill="both", expand=True)
        self._overview_tab()
        self._analysis_tab()
        self._data_tab()

    # ─────────────────  OVERVIEW TAB  ─────────────────────────
    def _overview_tab(self):
        tab = tb.Frame(self.nb); self.nb.add(tab, text="Overview")
        self._filters(tab)
        self._cards(tab)
        self._overview_fig(tab)

    def _spin(self, parent, **kw):
        return tb.Spinbox(parent, style="White.TSpinbox", **kw)

    def _filters(self, parent):
        f = tb.Labelframe(parent, text="Filters", padding=6)
        f.pack(fill="x", padx=8, pady=6)
        tk.Label(f, text="Make").grid(row=0, column=0, sticky="w", padx=4)
        self.make = tk.StringVar(value="All")
        tb.Combobox(f, textvariable=self.make, state="readonly", width=14,
                    values=["All"] + sorted(self.df["make"].unique()),
                    bootstyle="dark")\
          .grid(row=0, column=1)
        self.make.trace_add("write", self._apply_filters)
        if "drivetrain" in self.df.columns:
            tk.Label(f, text="Drivetrain").grid(row=0, column=2, padx=(20, 4))
            self.drive = tk.StringVar(value="All")
            tb.Combobox(f, textvariable=self.drive, state="readonly", width=14,
                        values=["All"] + sorted(self.df["drivetrain"].dropna()
                                                .unique()),
                        bootstyle="dark")\
              .grid(row=0, column=3)
            self.drive.trace_add("write", self._apply_filters)
        pr_min, pr_max = self.df["price"].min(), self.df["price"].max()
        tk.Label(f, text="Price $").grid(row=0, column=4, padx=(20, 4))
        self.pmin = tk.DoubleVar(value=float(pr_min))
        self.pmax = tk.DoubleVar(value=float(pr_max))
        for col, var in [(5, self.pmin), (6, self.pmax)]:
            self._spin(f, from_=0, to=float(pr_max), textvariable=var,
                       width=10, increment=1000, bootstyle="secondary")\
              .grid(row=0, column=col)
        if "year" in self.df.columns:
            yr_min, yr_max = int(self.df["year"].min()), int(self.df["year"].max())
            tk.Label(f, text="Year").grid(row=0, column=7, padx=(20, 4))
            self.ymin = tk.IntVar(value=yr_min)
            self.ymax = tk.IntVar(value=yr_max)
            for col, var in [(8, self.ymin), (9, self.ymax)]:
                self._spin(f, from_=1900, to=2035, textvariable=var,
                           width=6, bootstyle="secondary")\
                  .grid(row=0, column=col)
        tb.Button(f, text="Apply Year/Price Filters",
                  bootstyle="primary-outline",
                  command=self._apply_filters)\
          .grid(row=0, column=10, padx=(30, 4))

    def _cards(self, parent):
        wrap = tb.Frame(parent); wrap.pack(fill="x", padx=8)
        self.cards = {}
        for lbl in ("Total Cars", "Average Price",
                    "Average Mileage", "Avg Rating"):
            card = tb.Frame(wrap, padding=6, relief="ridge", bootstyle="dark")
            card.pack(side="left", fill="x", expand=True, padx=4, pady=4)
            val = tb.Label(card, text="-", font=("Segoe UI", 16, "bold"),
                           foreground=self.clr.info)
            val.pack()
            tb.Label(card, text=lbl, foreground="white").pack()
            self.cards[lbl] = val

    def _overview_fig(self, parent):
        fr = tb.Frame(parent); fr.pack(fill="both", expand=True, padx=8, pady=6)
        self.ov_fig = plt.Figure(figsize=(18, 10), facecolor="#1e1e1e",
                                 constrained_layout=True)
        self.ov_canvas = FigureCanvasTkAgg(self.ov_fig, master=fr)
        self.ov_canvas.get_tk_widget().pack(fill="both", expand=True)

    # ───────────────── ANALYSIS TAB ──────────────────────────
    def _analysis_tab(self):
        tab = tb.Frame(self.nb); self.nb.add(tab, text="Analysis")
        ctl = tb.Frame(tab); ctl.pack(fill="x", padx=8, pady=6)
        def set_and_run_analysis(plot_function):
            self.current_analysis_plot_func = plot_function
            plot_function()
        for txt, fn in (("Correlation", self._corr),
                        ("Price by Make", self._price_make),
                        ("MPG", self._mpg),
                        ("Ratings", self._ratings)):
            tb.Button(ctl, text=txt, command=lambda f=fn: set_and_run_analysis(f),
                      bootstyle="info-outline").pack(side="left", padx=4)
        self.an_fig = plt.Figure(figsize=(12, 7), facecolor="#1e1e1e",
                                 constrained_layout=True)
        self.an_canvas = FigureCanvasTkAgg(self.an_fig, master=tab)
        w = self.an_canvas.get_tk_widget()
        w.configure(width=1200, height=700)
        w.pack(padx=8, pady=4)

    # ───────────────── DATA TAB ────────────────────────────────
    def _data_tab(self):
        tab = tb.Frame(self.nb); self.nb.add(tab, text="Data")
        top = tb.Frame(tab); top.pack(fill="x", padx=8, pady=6)
        tk.Label(top, text="Search").pack(side="left")
        self.search = tk.StringVar()
        tk.Entry(top, textvariable=self.search, width=25)\
          .pack(side="left", padx=4)
        self.search.trace_add("write", self._search_tree)
        cols = list(self.df.columns)
        self.tree = tb.Treeview(tab, columns=cols, show="headings",
                                bootstyle="dark")
        for c in cols:
            self.tree.heading(c, text=c.title())
            self.tree.column(c, width=120, anchor="w")
        ysb = tb.Scrollbar(tab, orient="vertical", command=self.tree.yview)
        xsb = tb.Scrollbar(tab, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)
        self.tree.pack(side="left", fill="both", expand=True)
        ysb.pack(side="right", fill="y"); xsb.pack(side="bottom", fill="x")

    # ───────────────── FILTER & STATS ──────────────────────────
    def _apply_filters(self, *_):
        df = self.df.copy()
        if self.make.get() != "All":
            df = df[df["make"] == self.make.get()]
        if hasattr(self, "drive") and self.drive.get() != "All":
            df = df[df["drivetrain"] == self.drive.get()]
        try:
            pmin, pmax = float(self.pmin.get()), float(self.pmax.get())
        except ValueError:
            pmin, pmax = df["price"].min(), df["price"].max()
        df = df[(df["price"] >= pmin) & (df["price"] <= pmax)]
        if "year" in df.columns and hasattr(self, "ymin"):
            try:
                ymin, ymax = int(self.ymin.get()), int(self.ymax.get())
            except ValueError:
                ymin, ymax = df["year"].min(), df["year"].max()
            df = df[(df["year"] >= ymin) & (df["year"] <= ymax)]
        self.filtered = df
        self._update_cards()
        self._draw_overview()
        self._fill_tree()
        if self.current_analysis_plot_func:
            self.current_analysis_plot_func()

    def _update_cards(self):
        d = self.filtered
        self.cards["Total Cars"].configure(text=f"{len(d):,}")
        self.cards["Average Price"].configure(
            text=f"${d['price'].mean():,.0f}" if not d.empty else "$0"
        )
        m = d["mileage"].mean() if "mileage" in d.columns else np.nan
        self.cards["Average Mileage"].configure(
            text=f"{m:,.0f} mi" if not np.isnan(m) else "-"
        )
        r = d["consumerrating"].mean() if "consumerrating" in d.columns else np.nan
        self.cards["Avg Rating"].configure(
            text=f"{r:.2f}" if not np.isnan(r) else "-"
        )

    # ───────────────── OVERVIEW PLOTS (clickable) ──────────────
    def _draw_overview(self):
        if hasattr(self, "_ov_pick_id"):
            self.ov_fig.canvas.mpl_disconnect(self._ov_pick_id)
        
        self.ov_fig.clear()
        self._ov_annot = None 

        df = self.filtered
        if df.empty:
            ax = self.ov_fig.add_subplot(111)
            ax.axis("off")
            ax.text(0.5, 0.5, "No data", ha="center", va="center", color="white", fontsize=16)
            self.ov_canvas.draw(); return

        gs = self.ov_fig.add_gridspec(2, 2)
        
        ax_hist = self.ov_fig.add_subplot(gs[0, 0])
        ax_scatter = self.ov_fig.add_subplot(gs[0, 1])
        ax_pie = self.ov_fig.add_subplot(gs[1, 0])
        ax_bar = self.ov_fig.add_subplot(gs[1, 1])
        
        ax_hist.hist(df["price"], bins=30, color=self.clr.info)
        ax_hist.set_title("Price Distribution", color="w")
        ax_hist.set_xlabel("Price ($)", color="w"); ax_hist.set_ylabel("Cars", color="w")
        ax_hist.tick_params(colors="w")

        df_scatter_data = df.dropna(subset=["mileage", "price"])
        self._ov_scatter_map = {}
        if not df_scatter_data.empty:
            sc = ax_scatter.scatter(df_scatter_data["mileage"], df_scatter_data["price"],
                                    s=45, alpha=0.8, c=df_scatter_data["year"], cmap="viridis")
            sc.set_picker(True); sc.set_pickradius(10)
            self._ov_scatter_map[sc] = df_scatter_data.reset_index(drop=True)
            cb = self.ov_fig.colorbar(sc, ax=ax_scatter)
            cb.ax.yaxis.set_major_locator(MaxNLocator(integer=True))
            cb.ax.tick_params(colors="w"); cb.set_label("Year", color="w")

            def _on_pick(event):
                if len(event.ind) == 0:
                    return
                row = self._ov_scatter_map[event.artist].iloc[event.ind[0]]
                label = shorten(f"{row['make']} {row.get('model','')}", width=40, placeholder="…")
                if self._ov_annot:
                    self._ov_annot.remove()
                self._ov_annot = ax_scatter.annotate(
                    label, (row["mileage"], row["price"]),
                    xytext=(10, 10), textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="white", alpha=0.9), color="black"
                )
                self.ov_canvas.draw_idle()
            self._ov_pick_id = self.ov_fig.canvas.mpl_connect("pick_event", _on_pick)

        ax_scatter.set_title("Mileage vs Price", color="w")
        ax_scatter.set_xlabel("Mileage", color="w"); ax_scatter.set_ylabel("Price ($)", color="w")
        ax_scatter.tick_params(colors="w")

        if "drivetrain" in df.columns:
            cnt = df["drivetrain"].value_counts()
            if not cnt.empty:
                ax_pie.pie(cnt, labels=cnt.index, autopct="%1.0f%%", textprops={'color': 'w'})
            ax_pie.set_title("Cars by Drivetrain", color="w")

        if not df.empty:
            top = df.groupby("make")["price"].mean().nlargest(10).sort_values()
            if not top.empty:
                top.plot(kind="barh", ax=ax_bar, color=self.clr.primary)
        ax_bar.set_title("Top-10 Makes by Avg Price", color="w")
        ax_bar.set_xlabel("Average Price ($)", color="w"); ax_bar.set_ylabel("Make", color="w")
        ax_bar.tick_params(colors="w")

        self.ov_canvas.draw()

    # ───────────────── ANALYSIS PLOTS ──────────────────────────
    def _corr(self):
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        
        num = self.filtered.select_dtypes(include=np.number)
        if num.shape[1] < 2:
            ax.text(0.5, 0.5, "Not Enough Numeric Data", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
        
        im = ax.imshow(num.corr(), cmap="RdYlBu_r", vmin=-1, vmax=1)
        ax.set_xticks(range(num.shape[1])); ax.set_yticks(range(num.shape[1]))
        ax.set_xticklabels(num.columns, rotation=45, ha="right", color="w")
        ax.set_yticklabels(num.columns, color="w")
        cb = self.an_fig.colorbar(im, ax=ax, fraction=0.046)
        cb.ax.tick_params(colors="w"); cb.set_label("Correlation", color="w")
        ax.set_title("Feature Correlation Heat-map", color="w")
        self.an_canvas.draw()

    def _price_make(self):
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        
        df = self.filtered
        if df.empty or {"make","price"}.issubset(df.columns) is False:
            ax.text(0.5, 0.5, "No Data for this Filter", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return

        makes = df["make"].value_counts().nlargest(15).index
        if makes.empty:
            ax.text(0.5, 0.5, "No Makes to Display", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
            
        data  = [df[df["make"] == m]["price"] for m in makes]
        # ### FIX: Use 'labels' instead of 'tick_labels' ###
        ax.boxplot(data, labels=makes, vert=False, patch_artist=True,
                   boxprops=dict(facecolor=self.clr.info),
                   medianprops=dict(color=self.clr.danger))
        ax.set_title("Price Distribution by Make", color="w")
        ax.set_xlabel("Price ($)", color="w"); ax.set_ylabel("Make", color="w")
        ax.tick_params(colors="w")
        self.an_canvas.draw()

    def _ratings(self):
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        
        cols = [c for c in (
            "consumerrating","comfortrating","interiordesignrating",
            "performancerating","valueformoneyrating","reliabilityrating")
            if c in self.filtered.columns]
        
        if not cols:
            ax.text(0.5, 0.5, "No Rating Data in CSV", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
            
        data = self.filtered[cols].dropna()
        if data.empty:
            ax.text(0.5, 0.5, "No Rating Data for this Filter", ha="center", va="center", color="white", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return

        ax.boxplot(data.values,
                   labels=[c.replace("rating","") for c in cols],
                   patch_artist=True,
                   boxprops=dict(facecolor=self.clr.warning),
                   medianprops=dict(color=self.clr.danger))
        ax.set_title("Ratings Distribution", color="w")
        ax.set_ylabel("Rating (out of 5)", color="w"); ax.set_xlabel("Rating Type", color="w")
        ax.tick_params(colors="w", rotation=45)
        self.an_canvas.draw()

    def _mpg(self):
        if hasattr(self, "_mpg_pick_id"):
            self.an_fig.canvas.mpl_disconnect(self._mpg_pick_id)
        self.an_fig.clear()
        ax = self.an_fig.add_subplot(111)
        self._mpg_annot = None
        
        raw = self.filtered
        if {"minmpg","maxmpg","make"}.issubset(raw.columns) is False:
            ax.text(0.5,0.5,"No MPG Data in CSV",ha="center",va="center",color="w", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return
            
        df = raw.dropna(subset=["minmpg","maxmpg"])
        if df.empty:
            ax.text(0.5,0.5,"No MPG Data for this Filter",ha="center",va="center",color="w", fontsize=16)
            ax.axis('off')
            self.an_canvas.draw(); return

        top = df["make"].value_counts().nlargest(6).index
        palette = plt.cm.tab10.colors
        self._scatter_map = {}
        rest = df[~df["make"].isin(top)]
        if not rest.empty:
            sc = ax.scatter(rest["minmpg"], rest["maxmpg"],
                            s=25, c="lightgrey", alpha=.45, label="Other")
            sc.set_picker(True); sc.set_pickradius(10)
            self._scatter_map[sc] = rest.reset_index(drop=True)
        for i, mk in enumerate(top):
            sub = df[df["make"] == mk]
            sc = ax.scatter(sub["minmpg"], sub["maxmpg"],
                            s=35, color=palette[i % 10], label=mk, alpha=.8)
            sc.set_picker(True); sc.set_pickradius(10)
            self._scatter_map[sc] = sub.reset_index(drop=True)
        def _on_pick(event):
            if len(event.ind) == 0:
                return
            row = self._scatter_map[event.artist].iloc[event.ind[0]]
            label = shorten(f"{row['make']} {row.get('model','')}", width=40, placeholder="…")
            if self._mpg_annot: self._mpg_annot.remove()
            self._mpg_annot = ax.annotate(
                label, (row["minmpg"], row["maxmpg"]),
                xytext=(10, 10), textcoords="offset points",
                bbox=dict(boxstyle="round", fc="white", alpha=0.9), color="black"
            )
            self.an_canvas.draw_idle()
        self._mpg_pick_id = self.an_fig.canvas.mpl_connect("pick_event", _on_pick)
        try:
            best_hwy  = df.loc[df["maxmpg"].idxmax()]
            best_city = df.loc[df["minmpg"].idxmax()]
            for r, t in [(best_hwy, "Best Hwy"), (best_city, "Best City")]:
                ax.annotate(
                    f"{t}: {shorten(r['make']+' '+str(r.get('model','')),28, placeholder='…')}",
                    xy=(r["minmpg"], r["maxmpg"]),
                    xytext=(5, 5), textcoords="offset points",
                    fontsize=7, color="w", backgroundcolor="#00000080"
                )
        except (ValueError, KeyError): pass
        ax.set_title("City MPG vs Highway MPG", color="w")
        ax.set_xlabel("City MPG", color="w"); ax.set_ylabel("Highway MPG", color="w")
        ax.tick_params(colors="w")
        if len(top) > 0:
            ax.legend(facecolor="#1e1e1e", framealpha=.3, fontsize=8, labelcolor="w", loc="upper left")
        self.an_canvas.draw()

    # ───────────── TABLE / SEARCH / EXPORT ─────────────────────
    def _fill_tree(self):
        self.tree.delete(*self.tree.get_children())
        for _, row in self.filtered.head(500).iterrows():
            vals = [f"{v:,.2f}" if isinstance(v, float)
                    else f"{int(v):,}" if isinstance(v, (int, np.integer)) else v
                    for v in row]
            self.tree.insert("", "end", values=vals)

    def _search_tree(self, *_):
        term = self.search.get().lower()
        self.tree.delete(*self.tree.get_children())
        if not term: self._fill_tree(); return
        mask = self.filtered.astype(str).apply(

Entradas populares de este blog

Event Driven Architecture & Big ball of mud

EDA Una arquitectura event-driven (EDA) es un estilo de diseño que se basa en la producción, detección y reacción a eventos. Un evento es un cambio de estado significativo en el sistema o en el entorno que puede ser notificado a otros componentes interesados. Una arquitectura event-driven permite una mayor desacoplamiento, escalabilidad y resiliencia entre los componentes del sistema, así como una mejor adaptabilidad a los cambios y a las necesidades del negocio. Sin embargo, una arquitectura event-driven también puede tener sus desafíos y riesgos, especialmente si no se aplica una buena gestión de los dominios y los boundaries. Un dominio es un conjunto de conceptos, reglas y procesos relacionados con un aspecto del negocio o del problema que se quiere resolver. Un boundary es una frontera lógica que separa y protege un dominio de otros dominios o de influencias externas. Un buen diseño de dominios y boundaries facilita la comprensión, el mantenimiento y la evolución del sistema, así ...

¿Qué es el patrón Circuit Breaker y cómo se puede implementar con AWS Step Functions?

En el desarrollo de software, es común que las aplicaciones se comuniquen con servicios o recursos externos, como bases de datos, APIs o microservicios. Sin embargo, estos servicios o recursos pueden fallar o estar temporalmente indisponibles por diversas razones, lo que puede afectar el rendimiento y la disponibilidad de la aplicación. Para manejar estos escenarios de falla, se puede utilizar el patrón Circuit Breaker, que consiste en detectar y prevenir que una operación que tiene alta probabilidad de fallar se ejecute repetidamente, causando más problemas o consumiendo recursos innecesarios.  El patrón Circuit Breaker tiene tres estados posibles: cerrado, abierto y medio abierto. Cerrado : En este estado, el circuito está funcionando normalmente y la operación se ejecuta sin problemas. Si se detecta una falla, se incrementa un contador de fallas y se calcula un umbral de fallas, que puede ser un número o un porcentaje de fallas permitidas. Si el contador de fallas supera el u...

¿Cómo usar Lambda con Amazon SQS para procesar mensajes de forma asíncrona y escalable?

Amazon Simple Queue Service (Amazon SQS) es un servicio de colas de mensajes que permite enviar y recibir mensajes entre componentes de una aplicación de forma fiable y duradera. Con Amazon SQS, se puede desacoplar la lógica de negocio de la fuente de los eventos, y procesarlos de forma asíncrona y en paralelo.   En este artículo, vamos a ver cómo usar Lambda con Amazon SQS para procesar mensajes de una cola de forma eficiente y flexible, aprovechando las características de concurrencia, escalamiento y procesamiento del event source mapping de Lambda, así como la estrategia de backoff que implementa Lambda para manejar errores y reintentos.   Concurrencia del event source mapping Un event source mapping es una configuración que le dice a Lambda qué fuente de eventos debe monitorear y qué función debe invocar cuando se produzca un evento. En el caso de Amazon SQS, el event source mapping se encarga de leer los mensajes de la cola y enviarlos a la función Lambda en lotes. La con...