Tutorial: Dashboard Profesional en JavaFX

Construye paso a paso un dashboard moderno con sidebar, stat cards animadas, gráfico de barras y tabla de actividad. Diseño dark premium con componentes modulares reutilizables.

J

JavaFX Master

5 de marzo de 2025 25 min de lectura

Tutorial: Dashboard Profesional en JavaFX

En este tutorial construiremos un dashboard profesional completo desde cero con un diseño dark premium. El proyecto es completamente modular — cada componente (sidebar, topbar, tarjetas, tabla) vive en su propio archivo Java.

Al final podrás descargar el proyecto completo como un ZIP listo para ejecutar.

Resultado Final

El dashboard incluye:

  • Sidebar con navegación y perfil de usuario
  • Top Bar con búsqueda y avatar
  • 4 Stat Cards con contadores animados
  • Gráfico de barras de ventas mensuales
  • Panel de resumen rápido
  • Tabla de actividad reciente

Descargar Proyecto Completo

Descargar dashboard-javafx.zip

Estructura del Proyecto

El proyecto está diseñado de forma modular para mantener cada archivo enfocado y manejable:

dashboard-javafx/
├── src/main/java/com/dashboard/
│   ├── App.java              # Punto de entrada
│   ├── Sidebar.java          # Barra lateral de navegación
│   ├── TopBar.java           # Barra superior
│   ├── StatCard.java         # Tarjeta de estadísticas
│   ├── ChartPanel.java       # Panel de gráfico de barras
│   ├── ActivityTable.java    # Tabla de actividad reciente
│   └── DashboardView.java    # Vista principal que compone todo
├── src/main/resources/css/
│   └── dashboard.css          # Todos los estilos
└── run.bat                    # Compilar y ejecutar

Paso 1: App.java — Punto de Entrada

El punto de entrada es simple: crea un BorderPane con el sidebar a la izquierda y el contenido principal al centro.

package com.dashboard;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage stage) {
        BorderPane root = new BorderPane();
        root.getStyleClass().add("root-container");

        // Sidebar a la izquierda
        Sidebar sidebar = new Sidebar();
        root.setLeft(sidebar);

        // Contenido principal (TopBar + Dashboard)
        BorderPane mainContent = new BorderPane();
        mainContent.getStyleClass().add("main-content");

        TopBar topBar = new TopBar();
        mainContent.setTop(topBar);

        DashboardView dashboard = new DashboardView();
        mainContent.setCenter(dashboard);

        root.setCenter(mainContent);

        Scene scene = new Scene(root, 1280, 720);
        scene.getStylesheets().add(
            getClass().getResource("/css/dashboard.css").toExternalForm()
        );

        stage.setTitle("Dashboard JavaFX");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Concepto clave: Usamos BorderPane como layout raíz porque nos permite colocar el sidebar en LEFT y el contenido principal en CENTER. El contenido principal a su vez es otro BorderPane con el topbar en TOP y el dashboard scrolleable en CENTER.

Paso 2: Sidebar.java — Navegación Lateral

El sidebar contiene el logo, dos secciones de menú y un perfil de usuario al fondo:

package com.dashboard;

import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

public class Sidebar extends VBox {

    private String activeItem = "Dashboard";

    public Sidebar() {
        getStyleClass().add("sidebar");
        setPrefWidth(240);
        setMinWidth(240);

        // Logo
        Label logo = new Label("⚡ Dashboard");
        logo.getStyleClass().add("sidebar-logo");

        // Sección principal
        Label seccionPrincipal = new Label("PRINCIPAL");
        seccionPrincipal.getStyleClass().add("sidebar-section");

        VBox menuPrincipal = new VBox(2);
        menuPrincipal.getChildren().addAll(
            crearMenuItem("📊", "Dashboard", true),
            crearMenuItem("📈", "Analíticas", false),
            crearMenuItem("👥", "Usuarios", false),
            crearMenuItem("📦", "Productos", false)
        );

        // Sección gestión
        Label seccionGestion = new Label("GESTIÓN");
        seccionGestion.getStyleClass().add("sidebar-section");

        VBox menuGestion = new VBox(2);
        menuGestion.getChildren().addAll(
            crearMenuItem("💰", "Ventas", false),
            crearMenuItem("📋", "Reportes", false),
            crearMenuItem("⚙️", "Configuración", false)
        );

        // Espaciador para empujar el perfil al fondo
        Region spacer = new Region();
        VBox.setVgrow(spacer, Priority.ALWAYS);

        // Perfil de usuario
        VBox perfil = crearPerfil();

        getChildren().addAll(
            logo,
            seccionPrincipal, menuPrincipal,
            seccionGestion, menuGestion,
            spacer,
            perfil
        );
    }

    private VBox crearMenuItem(String icono, String texto, boolean activo) {
        Label iconLabel = new Label(icono);
        iconLabel.getStyleClass().add("menu-icon");

        Label textLabel = new Label(texto);
        textLabel.getStyleClass().add("menu-text");

        VBox item = new VBox();
        item.getStyleClass().add("menu-item");
        if (activo) {
            item.getStyleClass().add("menu-item-active");
        }

        javafx.scene.layout.HBox hbox = new javafx.scene.layout.HBox(10);
        hbox.setAlignment(Pos.CENTER_LEFT);
        hbox.getChildren().addAll(iconLabel, textLabel);
        item.getChildren().add(hbox);

        item.setOnMouseClicked(e -> {
            item.getStyleClass().add("menu-item-active");
            activeItem = texto;
        });

        return item;
    }

    private VBox crearPerfil() {
        VBox perfil = new VBox(4);
        perfil.getStyleClass().add("sidebar-profile");

        javafx.scene.layout.HBox hbox = new javafx.scene.layout.HBox(10);
        hbox.setAlignment(Pos.CENTER_LEFT);

        Label avatar = new Label("JA");
        avatar.getStyleClass().add("profile-avatar");

        VBox info = new VBox(2);
        Label nombre = new Label("Jorge Arroyo");
        nombre.getStyleClass().add("profile-name");
        Label rol = new Label("Administrador");
        rol.getStyleClass().add("profile-role");
        info.getChildren().addAll(nombre, rol);

        hbox.getChildren().addAll(avatar, info);
        perfil.getChildren().add(hbox);

        return perfil;
    }
}

Técnicas importantes:

  • VBox.setVgrow(spacer, Priority.ALWAYS) empuja el perfil de usuario hasta el fondo
  • getStyleClass().add() aplica las clases CSS que definiremos más adelante
  • El item activo se resalta con la clase menu-item-active

Paso 3: TopBar.java — Barra Superior

Una barra con el título de la página, un campo de búsqueda y el avatar del usuario:

package com.dashboard;

import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;

public class TopBar extends HBox {

    public TopBar() {
        getStyleClass().add("topbar");
        setAlignment(Pos.CENTER_LEFT);
        setSpacing(16);

        // Título de la página
        Label titulo = new Label("Dashboard");
        titulo.getStyleClass().add("topbar-title");

        Label subtitulo = new Label("Resumen general del sistema");
        subtitulo.getStyleClass().add("topbar-subtitle");

        javafx.scene.layout.VBox tituloBox = new javafx.scene.layout.VBox(2);
        tituloBox.getChildren().addAll(titulo, subtitulo);

        // Espaciador
        Region spacer = new Region();
        HBox.setHgrow(spacer, Priority.ALWAYS);

        // Barra de búsqueda
        TextField buscar = new TextField();
        buscar.setPromptText("🔍 Buscar...");
        buscar.getStyleClass().add("topbar-search");
        buscar.setPrefWidth(220);

        // Notificaciones
        Label notificaciones = new Label("🔔");
        notificaciones.getStyleClass().add("topbar-icon");

        // Avatar
        Label avatar = new Label("JA");
        avatar.getStyleClass().add("topbar-avatar");

        getChildren().addAll(tituloBox, spacer, buscar, notificaciones, avatar);
    }
}

Paso 4: StatCard.java — Tarjetas con Contadores Animados

Cada tarjeta muestra un KPI con un contador que se anima de 0 al valor final:

package com.dashboard;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.util.Duration;

public class StatCard extends VBox {

    public StatCard(String titulo, int valor, String formato, String tendencia,
                    boolean positiva, String icono, String colorClass) {
        getStyleClass().addAll("stat-card", colorClass);
        setSpacing(8);

        // Header: título + icono
        HBox header = new HBox();
        header.setAlignment(Pos.CENTER_LEFT);

        Label tituloLabel = new Label(titulo);
        tituloLabel.getStyleClass().add("stat-title");

        Region spacer = new Region();
        HBox.setHgrow(spacer, Priority.ALWAYS);

        Label iconoLabel = new Label(icono);
        iconoLabel.getStyleClass().add("stat-icon");

        header.getChildren().addAll(tituloLabel, spacer, iconoLabel);

        // Valor animado
        IntegerProperty valorProp = new SimpleIntegerProperty(0);
        Label valorLabel = new Label("0");
        valorLabel.getStyleClass().add("stat-value");

        valorProp.addListener((obs, oldVal, newVal) -> {
            valorLabel.setText(String.format(formato, newVal.intValue()));
        });

        Timeline timeline = new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(valorProp, 0)),
            new KeyFrame(Duration.millis(1200), new KeyValue(valorProp, valor))
        );
        timeline.play();

        // Tendencia
        Label tendenciaLabel = new Label(
            (positiva ? "↑ " : "↓ ") + tendencia
        );
        tendenciaLabel.getStyleClass().add(
            positiva ? "stat-trend-up" : "stat-trend-down"
        );

        getChildren().addAll(header, valorLabel, tendenciaLabel);
    }
}

La animación funciona así:

  1. Creamos un IntegerProperty que empieza en 0
  2. Un listener actualiza el Label cada vez que cambia
  3. Un Timeline anima la propiedad de 0 al valor final en 1.2 segundos

Paso 5: ChartPanel.java — Gráfico de Barras

Un gráfico de barras simple usando Rectangle:

package com.dashboard;

import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;

public class ChartPanel extends VBox {

    public ChartPanel() {
        getStyleClass().add("chart-card");
        setSpacing(0);

        // Header
        HBox header = new HBox();
        header.getStyleClass().add("card-header");
        header.setAlignment(Pos.CENTER_LEFT);

        Label titulo = new Label("Ventas Mensuales");
        titulo.getStyleClass().add("card-title");

        Region spacer = new Region();
        HBox.setHgrow(spacer, Priority.ALWAYS);

        Label periodo = new Label("Últimos 6 meses");
        periodo.getStyleClass().add("card-subtitle");

        header.getChildren().addAll(titulo, spacer, periodo);

        // Gráfico de barras
        HBox barras = new HBox(8);
        barras.setAlignment(Pos.BOTTOM_CENTER);
        barras.setPadding(new Insets(20, 16, 10, 16));
        VBox.setVgrow(barras, Priority.ALWAYS);

        String[] meses = {"Oct", "Nov", "Dic", "Ene", "Feb", "Mar"};
        double[] valores = {65, 45, 80, 55, 90, 72};
        double maxValor = 90;

        for (int i = 0; i < meses.length; i++) {
            barras.getChildren().add(crearBarra(meses[i], valores[i], maxValor));
        }

        getChildren().addAll(header, barras);
    }

    private VBox crearBarra(String mes, double valor, double maxValor) {
        VBox columna = new VBox(4);
        columna.setAlignment(Pos.BOTTOM_CENTER);
        HBox.setHgrow(columna, Priority.ALWAYS);

        double alturaMax = 180;
        double altura = (valor / maxValor) * alturaMax;

        Label valorLabel = new Label(String.format("$%.0fk", valor));
        valorLabel.getStyleClass().add("chart-value");

        Rectangle barra = new Rectangle();
        barra.setWidth(40);
        barra.setHeight(altura);
        barra.setArcWidth(6);
        barra.setArcHeight(6);
        barra.getStyleClass().add("chart-bar");

        Label mesLabel = new Label(mes);
        mesLabel.getStyleClass().add("chart-label");

        columna.getChildren().addAll(valorLabel, barra, mesLabel);
        return columna;
    }
}

Detalle clave: La altura de cada barra es proporcional al valor máximo usando (valor / maxValor) * alturaMax. Los Rectangle tienen arcWidth y arcHeight para esquinas redondeadas.

Paso 6: ActivityTable.java — Tabla de Actividad

Una tabla de actividad reciente con filas estilizadas:

package com.dashboard;

import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

public class ActivityTable extends VBox {

    public ActivityTable() {
        getStyleClass().add("activity-card");
        setSpacing(0);

        // Header
        HBox header = new HBox();
        header.getStyleClass().add("card-header");
        header.setAlignment(Pos.CENTER_LEFT);

        Label titulo = new Label("Actividad Reciente");
        titulo.getStyleClass().add("card-title");

        Region spacer = new Region();
        HBox.setHgrow(spacer, Priority.ALWAYS);

        Label verTodo = new Label("Ver todo →");
        verTodo.getStyleClass().add("card-link");

        header.getChildren().addAll(titulo, spacer, verTodo);

        // Cabecera de tabla
        HBox tableCabecera = crearFila(
            "Usuario", "Acción", "Fecha", "Estado", true
        );

        // Filas de datos
        VBox filas = new VBox(0);
        filas.getChildren().addAll(
            crearFila("Carlos López", "Compra #1042",
                      "Hace 5 min", "✅ Completado", false),
            crearFila("Ana Martínez", "Registro nuevo",
                      "Hace 12 min", "🔵 Nuevo", false),
            crearFila("Pedro García", "Compra #1041",
                      "Hace 23 min", "✅ Completado", false),
            crearFila("María Torres", "Soporte ticket",
                      "Hace 45 min", "🟡 Pendiente", false),
            crearFila("Luis Ramírez", "Compra #1040",
                      "Hace 1 hora", "✅ Completado", false),
            crearFila("Sofia Mendez", "Devolución",
                      "Hace 2 horas", "🔴 Cancelado", false)
        );

        getChildren().addAll(header, tableCabecera, filas);
    }

    private HBox crearFila(String col1, String col2,
                           String col3, String col4, boolean esHeader) {
        HBox fila = new HBox();
        fila.getStyleClass().add(
            esHeader ? "table-header" : "table-row"
        );
        fila.setAlignment(Pos.CENTER_LEFT);
        fila.setPadding(new Insets(12, 16, 12, 16));

        Label l1 = new Label(col1);
        Label l2 = new Label(col2);
        Label l3 = new Label(col3);
        Label l4 = new Label(col4);

        l1.setPrefWidth(160);
        l2.setPrefWidth(160);
        l3.setPrefWidth(120);
        l4.setPrefWidth(140);

        if (esHeader) {
            l1.getStyleClass().add("table-header-text");
            l2.getStyleClass().add("table-header-text");
            l3.getStyleClass().add("table-header-text");
            l4.getStyleClass().add("table-header-text");
        } else {
            l1.getStyleClass().add("table-cell-primary");
            l2.getStyleClass().add("table-cell");
            l3.getStyleClass().add("table-cell-muted");
            l4.getStyleClass().add("table-cell");
        }

        fila.getChildren().addAll(l1, l2, l3, l4);
        return fila;
    }
}

Paso 7: DashboardView.java — Composición Final

Este archivo une todos los componentes en un layout scrolleable:

package com.dashboard;

import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;

public class DashboardView extends ScrollPane {

    public DashboardView() {
        getStyleClass().add("dashboard-scroll");
        setFitToWidth(true);
        setHbarPolicy(ScrollBarPolicy.NEVER);

        VBox contenido = new VBox(20);
        contenido.setPadding(new Insets(24));
        contenido.getStyleClass().add("dashboard-content");

        // Fila de stat cards
        HBox statsRow = crearStatCards();

        // Fila de chart + resumen
        HBox chartsRow = crearChartsRow();

        // Tabla de actividad
        ActivityTable activityTable = new ActivityTable();

        contenido.getChildren().addAll(statsRow, chartsRow, activityTable);
        setContent(contenido);
    }

    private HBox crearStatCards() {
        HBox row = new HBox(16);

        StatCard ventas = new StatCard(
            "Ventas Totales", 24500, "$%,d",
            "+12.5% vs mes pasado", true, "💰", "stat-blue"
        );
        StatCard usuarios = new StatCard(
            "Usuarios Activos", 1847, "%,d",
            "+8.2% vs mes pasado", true, "👥", "stat-green"
        );
        StatCard pedidos = new StatCard(
            "Pedidos Nuevos", 356, "%,d",
            "-3.1% vs mes pasado", false, "📦", "stat-amber"
        );
        StatCard tasa = new StatCard(
            "Tasa de Conversión", 68, "%d%%",
            "+5.4% vs mes pasado", true, "📈", "stat-violet"
        );

        HBox.setHgrow(ventas, Priority.ALWAYS);
        HBox.setHgrow(usuarios, Priority.ALWAYS);
        HBox.setHgrow(pedidos, Priority.ALWAYS);
        HBox.setHgrow(tasa, Priority.ALWAYS);

        row.getChildren().addAll(ventas, usuarios, pedidos, tasa);
        return row;
    }

    private HBox crearChartsRow() {
        HBox row = new HBox(16);

        ChartPanel chart = new ChartPanel();
        HBox.setHgrow(chart, Priority.ALWAYS);

        VBox resumen = crearResumen();
        resumen.setPrefWidth(280);
        resumen.setMinWidth(280);

        row.getChildren().addAll(chart, resumen);
        return row;
    }

    private VBox crearResumen() {
        VBox card = new VBox(12);
        card.getStyleClass().add("summary-card");

        javafx.scene.control.Label titulo =
            new javafx.scene.control.Label("Resumen Rápido");
        titulo.getStyleClass().add("card-title");

        card.getChildren().add(titulo);

        String[][] items = {
            {"Ingresos hoy", "$3,420"},
            {"Tickets abiertos", "12"},
            {"Tiempo promedio", "2.4 min"},
            {"Satisfacción", "94%"},
            {"Tasa de error", "0.3%"}
        };

        for (String[] item : items) {
            HBox fila = new HBox();
            fila.getStyleClass().add("summary-row");

            javafx.scene.control.Label label =
                new javafx.scene.control.Label(item[0]);
            label.getStyleClass().add("summary-label");

            javafx.scene.layout.Region spacer =
                new javafx.scene.layout.Region();
            HBox.setHgrow(spacer, Priority.ALWAYS);

            javafx.scene.control.Label valor =
                new javafx.scene.control.Label(item[1]);
            valor.getStyleClass().add("summary-value");

            fila.getChildren().addAll(label, spacer, valor);
            card.getChildren().add(fila);
        }

        return card;
    }
}

Paso 8: dashboard.css — Los Estilos

Este es todo el CSS del dashboard. Es extenso pero organizado por secciones:

Variables y Root

.root-container {
    -fx-font-family: 'Segoe UI', system-ui, sans-serif;
    -fx-font-size: 14;
    -color-bg: #0f172a;
    -color-surface: #1e293b;
    -color-border: #334155;
    -color-text: #f1f5f9;
    -color-text-secondary: #94a3b8;
    -color-text-muted: #64748b;
    -color-primary: #3b82f6;
    -color-success: #10b981;
    -color-warning: #f59e0b;
    -color-danger: #ef4444;
    -fx-background-color: -color-bg;
}

Sidebar

.sidebar {
    -fx-background-color: #0c1222;
    -fx-padding: 20 16;
    -fx-spacing: 8;
    -fx-border-width: 0 1 0 0;
    -fx-border-color: -color-border;
}

.sidebar-logo {
    -fx-text-fill: white;
    -fx-font-size: 20;
    -fx-font-weight: bold;
    -fx-padding: 0 0 20 8;
}

.menu-item {
    -fx-padding: 10 12;
    -fx-background-radius: 8;
    -fx-cursor: hand;
}

.menu-item:hover {
    -fx-background-color: -color-surface;
}

.menu-item-active {
    -fx-background-color: -color-primary;
}

.menu-item-active .menu-text {
    -fx-text-fill: white;
    -fx-font-weight: bold;
}

Stat Cards

.stat-card {
    -fx-padding: 20;
    -fx-background-color: -color-surface;
    -fx-background-radius: 12;
    -fx-border-color: -color-border;
    -fx-border-radius: 12;
    -fx-border-width: 1;
}

.stat-value {
    -fx-text-fill: -color-text;
    -fx-font-size: 28;
    -fx-font-weight: bold;
}

.stat-icon {
    -fx-font-size: 24;
    -fx-padding: 8;
    -fx-background-radius: 10;
    -fx-min-width: 44;
    -fx-min-height: 44;
    -fx-alignment: center;
}

/* Colores por variante */
.stat-blue .stat-icon {
    -fx-background-color: rgba(59, 130, 246, 0.15);
}
.stat-green .stat-icon {
    -fx-background-color: rgba(16, 185, 129, 0.15);
}
.stat-amber .stat-icon {
    -fx-background-color: rgba(245, 158, 11, 0.15);
}
.stat-violet .stat-icon {
    -fx-background-color: rgba(139, 92, 246, 0.15);
}

.stat-trend-up {
    -fx-text-fill: -color-success;
    -fx-font-size: 12;
}

.stat-trend-down {
    -fx-text-fill: -color-danger;
    -fx-font-size: 12;
}

Tabla

.table-header {
    -fx-background-color: rgba(30, 41, 59, 0.6);
}

.table-header-text {
    -fx-text-fill: -color-text-muted;
    -fx-font-size: 12;
    -fx-font-weight: bold;
}

.table-row {
    -fx-border-width: 0 0 1 0;
    -fx-border-color: rgba(51, 65, 85, 0.5);
}

.table-row:hover {
    -fx-background-color: rgba(51, 65, 85, 0.3);
}

.table-cell-primary {
    -fx-text-fill: -color-text;
    -fx-font-weight: 600;
}

Gráfico de Barras

.chart-bar {
    -fx-fill: -color-primary;
}

.chart-bar:hover {
    -fx-fill: #2563eb;
}

Pro tip: En el archivo ZIP encontrarás el CSS completo con todas las secciones: sidebar profile, topbar, scrollbar personalizado, cards genéricos y más.

Paso 9: Ejecutar el Dashboard

  1. Descomprime el archivo ZIP descargado
  2. Abre run.bat y configura la ruta de tu JavaFX SDK:
set JAVAFX_PATH=E:\Java\javafx-sdk-24.0.1\lib
  1. Haz doble clic en run.bat

dashboard

Conceptos Clave Aprendidos

ConceptoDónde se usa
BorderPane para layouts complejosApp.java (sidebar + contenido)
VBox.setVgrow para espaciadoresSidebar (perfil al fondo)
HBox.setHgrow para distribuciónDashboardView (stat cards)
Timeline para animacionesStatCard (contador animado)
ScrollPane para contenido largoDashboardView
Rectangle para gráficos customChartPanel (barras)
CSS variables para temasdashboard.css (colores)
Pseudo-clases :hoverBotones, filas de tabla

Extendiendo el Dashboard

Algunas ideas para mejorar el dashboard:

  • Datos reales: Conecta una base de datos con JDBC para mostrar datos dinámicos
  • Gráficos avanzados: Usa las clases Chart de JavaFX (LineChart, PieChart)
  • Modo claro: Agrega un toggle que cambie las variables CSS
  • Navegación real: Haz que cada menú del sidebar cambie el contenido central
  • Responsive: Usa listeners en widthProperty() para adaptar el layout

Siguiente Paso

¡Tu dashboard está listo! En los próximos artículos exploraremos cómo conectar datos reales, crear gráficos interactivos y agregar notificaciones toast animadas.

forumComentarios

Deja tu comentario

progress_activityCargando comentarios...