使用-HTML-CSS-和-JavaScript-构建数据仪表盘

使用 HTML、CSS 和 JavaScript 构建数据仪表盘

原文:towardsdatascience.com/build-a-data-dashboard-using-html-css-javascript/

为您的客户、客户或同事构建数据仪表盘正成为软件开发者、数据科学家、机器学习实践者和数据工程师所需技能集的重要组成部分。即使你主要在后台处理,你处理的数据通常需要在某个时候“呈现”给用户。如果你很幸运,你的组织可能有一个专门的前端团队来处理这个问题,但通常这会落到你身上。

对于没有 HTML、JavaScript 等经验的 Python 开发者来说,这不再是借口,因为过去几年中已经出现了许多 Python 库,例如 Streamlit 和 Gradio。

这篇文章并不是关于他们的,因为就是那些直言不讳的 Python 开发者之一,我已经尝试过 Streamlit 和 Gradio。所以现在是时候卷起袖子,看看我是否能够学习新技能并使用那些老牌前端开发技术:HTML、JavaScript 和 CSS 来创建仪表盘了。

我们仪表板的数据将来自本地 SQLite 数据库。我在 SQLite 中创建了一个名为sales_data的表,其中包含模拟的销售数据。以下是表格形式的数据。

图片

图片由作者提供

下面是一些你可以用来跟随并创建自己的 SQLite 数据库和表的代码,其中包含所示的数据。

如果你想知道为什么我只在我的数据库中插入了一小部分记录,那并不是因为我认为代码无法处理大量数据。只是我想专注于仪表盘功能,而不是被数据分散注意力。如果你喜欢,可以使用我下面提供的脚本向输入数据集添加额外的记录。

因此,我们在设置 SQLite 数据库时仍然停留在 Python 的世界中。

import sqlite3

# Define the database name
DATABASE_NAME = "C:\\Users\\thoma\\projects\\my-dashboard\\sales_data.db"

# Connect to SQLite database
conn = sqlite3.connect(DATABASE_NAME)

# Create a cursor object
cursor = conn.cursor()

# SQL to create the 'sales' table
create_table_query = '''
CREATE TABLE IF NOT EXISTS sales (
    order_id INTEGER PRIMARY KEY,
    order_date TEXT,
    customer_id INTEGER,
    customer_name TEXT,
    product_id INTEGER,
    product_names TEXT,
    categories TEXT,
    quantity INTEGER,
    price REAL,
    total REAL
);
'''

# Execute the query to create the table
cursor.execute(create_table_query)

# Sample data to insert into the 'sales' table
sample_data = [
    (1, "2022-08-01", 245, "Customer_884", 201, "Smartphone", "Electronics", 3, 90.02, 270.06),
    (2, "2022-02-19", 701, "Customer_1672", 205, "Printer", "Electronics", 6, 12.74, 76.44),
    (3, "2017-01-01", 184, "Customer_21720", 208, "Notebook", "Stationery", 8, 48.35, 386.80),
    (4, "2013-03-09", 275, "Customer_23770", 200, "Laptop", "Electronics", 3, 74.85, 224.55),
    (5, "2022-04-23", 960, "Customer_23790", 210, "Cabinet", "Office", 6, 53.77, 322.62),
    (6, "2019-07-10", 197, "Customer_25587", 202, "Desk", "Office", 3, 47.17, 141.51),
    (7, "2014-11-12", 510, "Customer_6912", 204, "Monitor", "Electronics", 5, 22.5, 112.5),
    (8, "2016-07-12", 150, "Customer_17761", 200, "Laptop", "Electronics", 9, 49.33, 443.97)
]

# SQL to insert data into the 'sales' table
insert_data_query = '''
INSERT INTO sales (order_id, order_date, customer_id, customer_name, product_id, product_names, categories, quantity, price, total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''

# Insert the sample data
cursor.executemany(insert_data_query, sample_data)

# Commit the transaction
conn.commit()

# Close the connection
conn.close()

print(f"Database '{DATABASE_NAME}' has been created and populated successfully.")

仪表盘功能

我们的仪表盘将具有以下功能。

  • 关键指标。总收入、总订单数、平均订单价值、顶级类别

  • 不同的图表类型。随时间变化的收入(折线图)、按类别划分的收入(柱状图)、按收入排名的前产品(水平柱状图)

  • 过滤。按日期和类别

  • 数据表。以分页和可搜索的网格格式显示我们的数据记录。

设置我们的环境

接下来,我们将遵循一系列步骤来设置我们的环境。

1/ 安装 Node.js

Node.js 是一个运行时环境,它允许你在浏览器之外运行 JavaScript,让你可以使用 JavaScript 构建快速且可扩展的服务器端应用程序。

因此,请确保您的系统已安装 Node.js,以便您能够运行本地服务器并管理包。您可以从 Node.js 官方网站 下载它。

2/ 创建主项目文件夹和子文件夹

打开您的命令终端并运行以下命令。我在 Windows 磁盘上使用 Ubuntu,但您可以根据您首选的命令行工具和系统进行更改。

$ mkdir my-dashboard
$ cd my-dashboard
$ mkdir client
% mkdir server

3/ 初始化 Node 项目

$ npm init -y

此命令会自动在您的项目目录中创建一个默认的 package.json 文件,而无需用户输入

-y 标志对所有提示回答 “是”,使用字段的 默认值,例如:

  • 名称

  • 版本

  • 描述

  • 脚本

  • 作者

  • 许可证

这就是我的包文件看起来像什么。

{
  "name": "my-dashboard",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "express": "⁴.21.2",
    "sqlite3": "⁵.1.7"
  }
}

4/ 安装 Express 和 SQLite

SQLite 是一个轻量级的基于文件的数据库引擎,它将所有数据存储在一个单一的、可移植的文件中,消除了需要单独服务器的需求。

Express 是一个用于 Node.js 的最小、灵活的 Web 应用程序框架,通过路由和中间件简化了 API 和 Web 服务器的构建。

我们可以使用以下命令安装它们。

$ npm install express sqlite3

现在,我们可以开始编写代码了。对于这个项目,我们需要四个代码文件:一个 index.html 文件、一个 server.js 文件、一个 client.js 文件和一个 script.js 文件。

让我们一步一步地了解它们。

1) 客户端/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
    <link rel="stylesheet" href="style.css">
    <title>Sales Performance Dashboard</title>
</head>
<body>
    <div class="container">
        <!-- Centered Heading -->
        <h1 class="text-center">Sales Performance Dashboard</h1>

        <!-- Filter Section -->
        <div class="filters row my-4">
            <div class="col-md-4">
                <label for="start-date">Start Date</label>
                <input type="text" id="start-date" class="form-control" placeholder="Start Date">
            </div>
            <div class="col-md-4">
                <label for="end-date">End Date</label>
                <input type="text" id="end-date" class="form-control" placeholder="End Date">
            </div>
            <div class="col-md-4">
                <label for="category-filter">Category</label>
                <select id="category-filter" class="form-control">
                    <option value="all">All Categories</option>
                    <!-- Options will be populated dynamically -->
                </select>
            </div>
        </div>

        <!-- Key Metrics Section -->
        <h2 class="mt-5">Key Metrics</h2> <!-- Added heading for Key Metrics -->
        <div id="key-metrics" class="row text-center my-4">
            <div class="col-md-3">
                <h4>Total Revenue</h4>
                <p id="total-revenue">$0</p>
            </div>
            <div class="col-md-3">
                <h4>Total Orders</h4>
                <p id="total-orders">0</p>
            </div>
            <div class="col-md-3">
                <h4>Average Order Value</h4>
                <p id="average-order-value">$0</p>
            </div>
            <div class="col-md-3">
                <h4>Top Category</h4>
                <p id="top-category">None</p>
            </div>
        </div>

        <!-- Chart Section -->
        <div class="chart-section my-4">
            <label for="chart-type-selector">Select Chart:</label>
            <select id="chart-type-selector" class="form-control mb-3">
                <option value="revenueOverTime">Revenue Over Time</option>
                <option value="revenueByCategory">Revenue By Category</option>
                <option value="topProducts">Top Products by Revenue</option>
            </select>
            <canvas id="chart-canvas"></canvas>
        </div>

        <!-- Raw Data Table Section -->
        <div id="raw-data" class="my-4">
            <h3>Raw Data</h3>
            <table id="data-table" class="table table-striped table-bordered"></table>
        </div>
    </div>

    <!-- Required JS Libraries -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
    <script src="script.js"></script>
</body>
</html>

此 HTML 文件建立了我们的销售绩效仪表板的基本视觉元素,包括日期和类别的交互式过滤器、显示关键销售指标的章节、选择图表类型的下拉菜单以及用于原始数据的表格。

Bootstrap 用于样式设计。Flatpickr 用于日期输入。Chart.js 用于可视化,DataTables 用于表格显示。交互性由外部的 script.js 文件处理,我们稍后会对其进行检查。

Bootstrap 是一个流行的前端框架,最初由 Twitter 开发,它帮助您更轻松、更快速地构建响应式和视觉上一致的 Web 界面。

DataTables 是一个基于 jQuery 的插件,它增强了标准的 HTML

元素,将它们转换成完全交互式、功能丰富的表格。

Flatpickr 是一个轻量级、可定制的 JavaScript 日期和时间选择器。它允许用户从简洁的弹出日历中选择日期(以及可选的时间),而不是手动输入。

Chart.js 是一个简单而强大的 JavaScript 库,用于在 Web 应用程序中使用 元素创建交互式、动画图表。

2) 客户端/style.css

/* client/style.css */
body {
    background-color: #f8f9fa;
    font-family: 'Arial', sans-serif;
}

h1 {
    text-align: center; /* Center the heading */
    margin-top: 20px; /* Add spacing above the heading */
    margin-bottom: 40px; /* Add spacing below the heading */
}

.container .filters {
    margin-top: 20px;
    margin-bottom: 60px !important; /* Ensure larger spacing between filters and Key Metrics */
}

.container #key-metrics {
    margin-top: 40px !important; /* Additional spacing above the Key Metrics section */
    margin-bottom: 20px; /* Optional spacing below */
}

.key-metrics div {
    margin: 10px 0;
    padding: 10px;
    background-color: #f4f4f4;
    border: 1px solid #ccc;
    border-radius: 4px;
}

/* Fix for DataTables Pagination Spacing */
.dataTables_wrapper .dataTables_paginate {
    text-align: center;
    margin-top: 10px;
}

.dataTables_wrapper .dataTables_paginate .paginate_button {
    margin: 0 12px;
    padding: 5px 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    background-color: #f9f9f9;
    color: #007bff;
    text-decoration: none;
    display: inline-block;
}

.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
    background-color: #007bff;
    color: #fff;
    border: 1px solid #007bff;
}

.dataTables_wrapper .dataTables_paginate .paginate_button.current {
    font-weight: bold;
    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
}

我们使用级联样式表(CSS)来设计仪表板的基本视觉组件,例如按钮和文本颜色、元素间的间距等。

style.css 文件为仪表板提供外观和整体外观。它是一个干净、轻快的主题,具有充足的间距和布局调整,以提高清晰度和可读性。style.css 文件还自定义了 DataTables 分页按钮的外观,使其更用户友好,并与 Bootstrap 的设计视觉上保持一致。

3) server/server.js

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const app = express();
const PORT = 3000;

// Full path to your SQLite database
const DB_PATH = "C:\\Users\\thoma\\projects\\my-dashboard\\sales_data.db";

// Serve static files from the client directory
app.use(express.static(path.join(__dirname, '..', 'client')));

// Route to fetch data from SQLite database
app.get('/data', (req, res) => {
    const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READONLY, (err) => {
        if (err) {
            console.error("Error connecting to database:", err.message);
            res.status(500).json({ error: "Database connection failed" });
            return;
        }
    });

    // Query the database
    const query = "SELECT * FROM sales;"; // Replace 'sales' with your table name
    db.all(query, [], (err, rows) => {
        if (err) {
            console.error("Error running query:", err.message);
            res.status(500).json({ error: "Query failed" });
        } else {
            res.json(rows); // Send the query result as JSON
        }
    });

    db.close((err) => {
        if (err) {
            console.error("Error closing database:", err.message);
        }
    });
});

// Catch-all route to serve the main HTML file
app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, '..', 'client', 'index.html'));
});

// Start the server
app.listen(PORT, () => {
    console.log(`Server running at http://localhost:${PORT}`);
});

此 Node.js 脚本包含设置基本 Express 服务器所用的 JavaScript 代码,该服务器为销售绩效仪表板提供动力。它做两件事:

  1. 从客户端子文件夹中提供静态文件(如 HTML、CSS 和 JS),以便前端在浏览器中加载。

  2. 提供一个 /data 端点,该端点从本地 SQLite 数据库(sales_data.db)读取并返回整个销售表作为 JSON,从而在前端启用动态数据可视化和表格。

注意:在发布后,一位读者在运行上述代码时指出以下错误。

PS D:\repo\my-dashboard\server> node server.js

D:\repo\my-dashboard\node_modules\path-to-regexp\dist\index.js:96

            throw new PathError(`Missing parameter name at index ${index}`, str);

            ^

PathError [TypeError]: Missing parameter name at index 1: *; visit https://git.new/pathToRegexpError for info

    at name (D:\repo\my-dashboard\node_modules\path-to-regexp\dist\index.js:96:19)

    at parse (D:\repo\my-dashboard\node_modules\path-to-regexp\dist\index.js:113:68)

    at pathToRegexp (D:\repo\my-dashboard\node_modules\path-to-regexp\dist\index.js:267:58)

    at Object.match (D:\repo\my-dashboard\node_modules\path-to-regexp\dist\index.js:237:30)

    at matcher (D:\repo\my-dashboard\node_modules\router\lib\layer.js:86:23)

    at new Layer (D:\repo\my-dashboard\node_modules\router\lib\layer.js:93:62)

    at router.route (D:\repo\my-dashboard\node_modules\router\index.js:428:17)

    at app.route (D:\repo\my-dashboard\node_modules\express\lib\application.js:257:22)

    at app.<computed> [as get] (D:\repo\my-dashboard\node_modules\express\lib\application.js:478:22)

    at Object.<anonymous> (D:\repo\my-dashboard\server\server.js:42:5) {

  originalPath: '*'

}

问题似乎是由于他们使用的 Express 库版本引起的。我建议将此行,

app.get(‘*’, (req, res) => {

改为

app.get(‘/*’, (req, res) => {

最后,经过一些实验,他们发现将线条改为以下所示对他们有效。

app.get(‘/’, (req, res) => {

4) client/script.js

let chartInstance = null; // Global variable to store the current Chart.js instance

// Wait until the DOM is fully loaded
document.addEventListener('DOMContentLoaded', function () {
    // Fetch sales data from the backend API
    fetch('/data')
        .then((response) => response.json())
        .then((data) => {
            // Handle case where no data is returned
            if (!data || data.length === 0) {
                const app = document.getElementById('app');
                if (app) {
                    app.innerHTML = "<p>No data available.</p>";
                }
                return;
            }

            // Initialize filters and dashboard content
            setupFilters(data);
            initializeDashboard(data);

            // Re-render charts when chart type changes
            document.getElementById('chart-type-selector').onchange = () => filterAndRenderData(data);
        })
        .catch((error) => {
            // Handle fetch error
            console.error('Error fetching data:', error);
            const app = document.getElementById('app');
            if (app) {
                app.innerHTML = "<p>Failed to fetch data.</p>";
            }
        });
});

// Initialize Flatpickr date pickers and category filter
function setupFilters(data) {
    // Convert date strings to JS Date objects
    const dates = data.map((item) => new Date(item.order_date.split('/').reverse().join('-')));
    const minDate = new Date(Math.min(...dates));
    const maxDate = new Date(Math.max(...dates));

    // Configure start date picker
    flatpickr("#start-date", {
        defaultDate: minDate.toISOString().slice(0, 10),
        dateFormat: "Y-m-d",
        altInput: true,
        altFormat: "F j, Y",
        onChange: function () {
            filterAndRenderData(data);
        },
    });

    // Configure end date picker
    flatpickr("#end-date", {
        defaultDate: maxDate.toISOString().slice(0, 10),
        dateFormat: "Y-m-d",
        altInput: true,
        altFormat: "F j, Y",
        onChange: function () {
            filterAndRenderData(data);
        },
    });

    // Set up category dropdown change listener
    const categoryFilter = document.getElementById('category-filter');
    if (categoryFilter) {
        categoryFilter.onchange = () => filterAndRenderData(data);
    }
}

// Initialize dashboard after filters are set
function initializeDashboard(data) {
    populateCategoryFilter(data);     // Populate category dropdown
    filterAndRenderData(data);       // Initial render with all data
}

// Apply filters and update key metrics, chart, and table
function filterAndRenderData(data) {
    const chartType = document.getElementById('chart-type-selector').value;
    const startDate = document.getElementById('start-date')._flatpickr.selectedDates[0];
    const endDate = document.getElementById('end-date')._flatpickr.selectedDates[0];
    const selectedCategory = document.getElementById('category-filter').value;

    // Filter data by date and category
    const filteredData = data.filter((item) => {
        const itemDate = new Date(item.order_date.split('/').reverse().join('-'));
        return (
            itemDate >= startDate &&
            itemDate <= endDate &&
            (selectedCategory === 'all' || item.categories === selectedCategory)
        );
    });

    updateKeyMetrics(filteredData);                   // Update metrics like revenue and orders
    drawChart(filteredData, 'chart-canvas', chartType); // Render chart
    populateDataTable(filteredData);                  // Update table
}

// Update dashboard metrics (total revenue, order count, etc.)
function updateKeyMetrics(data) {
    const totalRevenue = data.reduce((acc, item) => acc + parseFloat(item.total), 0);
    const totalOrders = data.length;
    const averageOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;

    // Calculate total revenue per category to find top category
    const revenueByCategory = data.reduce((acc, item) => {
        const category = item.categories || "Uncategorized";
        acc[category] = (acc[category] || 0) + parseFloat(item.total);
        return acc;
    }, {});

    // Determine category with highest total revenue
    const topCategory = Object.keys(revenueByCategory).reduce(
        (a, b) => (revenueByCategory[a] > revenueByCategory[b] ? a : b),
        "None"
    );

    // Display metrics in the DOM
    document.getElementById('total-revenue').textContent = `$${totalRevenue.toFixed(2)}`;
    document.getElementById('total-orders').textContent = `${totalOrders}`;
    document.getElementById('average-order-value').textContent = `$${averageOrderValue.toFixed(2)}`;
    document.getElementById('top-category').textContent = topCategory || 'None';
}

// Draw the selected chart type using Chart.js
function drawChart(data, elementId, chartType) {
    const ctx = document.getElementById(elementId).getContext('2d');

    // Destroy previous chart if one exists
    if (chartInstance) {
        chartInstance.destroy();
    }

    switch (chartType) {
        case 'revenueOverTime':
            // Line chart showing revenue by order date
            chartInstance = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: data.map((item) => item.order_date),
                    datasets: [{
                        label: 'Revenue Over Time',
                        data: data.map((item) => parseFloat(item.total)),
                        fill: false,
                        borderColor: 'rgb(75, 192, 192)',
                        tension: 0.1,
                    }],
                },
                options: {
                    scales: {
                        y: { beginAtZero: true },
                    },
                },
            });
            break;

        case 'revenueByCategory':
            // Bar chart showing total revenue per category
            const categories = [...new Set(data.map((item) => item.categories))];
            const revenueByCategory = categories.map((category) => {
                return {
                    category,
                    revenue: data
                        .filter((item) => item.categories === category)
                        .reduce((acc, item) => acc + parseFloat(item.total), 0),
                };
            });
            chartInstance = new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: revenueByCategory.map((item) => item.category),
                    datasets: [{
                        label: 'Revenue by Category',
                        data: revenueByCategory.map((item) => item.revenue),
                        backgroundColor: 'rgba(255, 99, 132, 0.2)',
                        borderColor: 'rgba(255, 99, 132, 1)',
                        borderWidth: 1,
                    }],
                },
                options: {
                    scales: {
                        y: { beginAtZero: true },
                    },
                },
            });
            break;

        case 'topProducts':
            // Horizontal bar chart showing top 10 products by revenue
            const productRevenue = data.reduce((acc, item) => {
                const productName = item.product_names || 'Unknown Product';
                acc[productName] = (acc[productName] || 0) + parseFloat(item.total);
                return acc;
            }, {});

            const topProducts = Object.entries(productRevenue)
                .sort((a, b) => b[1] - a[1])
                .slice(0, 10);

            chartInstance = new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: topProducts.map((item) => item[0]), // Product names
                    datasets: [{
                        label: 'Top Products by Revenue',
                        data: topProducts.map((item) => item[1]), // Revenue
                        backgroundColor: 'rgba(54, 162, 235, 0.8)',
                        borderColor: 'rgba(54, 162, 235, 1)',
                        borderWidth: 1,
                    }],
                },
                options: {
                    indexAxis: 'y', // Horizontal bars
                    scales: {
                        x: { beginAtZero: true },
                    },
                },
            });
            break;
    }
}

// Display filtered data in a DataTable
function populateDataTable(data) {
    const tableElement = $('#data-table');

    // Destroy existing table if it exists
    if ($.fn.DataTable.isDataTable(tableElement)) {
        tableElement.DataTable().clear().destroy();
    }

    // Create a new DataTable with relevant columns
    tableElement.DataTable({
        data: data.map((item) => [
            item.order_id,
            item.order_date,
            item.customer_id,
            item.product_names,
            item.categories,
            `$${parseFloat(item.total).toFixed(2)}`,
        ]),
        columns: [
            { title: "Order ID" },
            { title: "Order Date" },
            { title: "Customer ID" },
            { title: "Product" },
            { title: "Category" },
            { title: "Total" },
        ],
    });
}

// Populate the category filter dropdown with available categories
function populateCategoryFilter(data) {
    const categoryFilter = document.getElementById('category-filter');
    categoryFilter.innerHTML = '';
    categoryFilter.appendChild(new Option('All Categories', 'all', true, true));

    // Extract unique categories
    const categories = new Set(data.map((item) => item.categories));
    categories.forEach((category) => {
        categoryFilter.appendChild(new Option(category, category));
    });
}

这是我们的最复杂的代码文件,但它必须做很多事情。此 JavaScript 文件为销售绩效仪表板的交互性和数据可视化提供动力。简而言之,它 …

1/ 获取销售数据

  • 当页面加载时 (DOMContentLoaded),它会在 /data 端点调用后端 API。

  • 如果没有返回数据,则显示“没有可用数据”消息。

2/ 设置过滤器

  • 使用 Flatpickr 日期选择器根据数据集的最小/最大订单日期选择开始和结束日期。

  • 添加一个 类别下拉菜单,允许用户按产品类别进行筛选。

  • 添加一个 图表类型选择器,以便在不同的图表可视化之间切换。

3/ 初始化仪表板

  • 用可用的类别填充类别过滤器。

  • 使用完整数据集运行第一次渲染。

4/ 应用过滤器并重新渲染

  • 每次用户更改过滤器(日期范围、类别或图表类型)时,它:

    • 通过日期范围和类别过滤数据集。

    • 更新 关键指标:总收入、订单数量、平均订单价值和最高收入类别。

    • 重新绘制选定的 Chart.js 图表

    • 刷新 数据表

5/ 使用 Chart.js 绘制图表

  • 随时间推移的收入 → 按日期显示收入趋势的折线图。

  • 按类别划分的收入 → 按类别汇总的总收入条形图。

  • 顶级产品 → 水平条形图显示按收入排名前 10 的产品。

6/ 显示表格数据

  • 使用 DataTables (一个 jQuery 插件) 渲染一个过滤订单的表格,包含订单 ID、日期、客户 ID、产品、类别和总计列。

7/ 保持 UI 同步

  • 当过滤器改变时销毁并重新创建图表/表格,以避免重复。

  • 保持指标、图表和表格与活动过滤器的一致性。

运行我们的仪表板

现在我们已经整理好了所有代码,是时候运行仪表板了,所以请转到服务器子文件夹,并输入以下命令。

$ node server.js

您将收到上述命令的响应,类似于,

Server running at http://localhost:3000

打开网页浏览器并访问http://localhost:3000。您应该会看到您的仪表板已用 SQLite 数据库中的数据填充,如下面的图片所示。

图片

图片由作者提供

所有过滤器、图表选择等都应该按预期工作。

摘要

在这篇文章中,我向您介绍了如何使用核心网络技术——HTML、CSS、JavaScript、Node.js、Express 和本地 SQLite 数据库创建一个功能齐全、交互式的销售绩效仪表板。

我们讨论了技术栈和设置。即:

  • 后端: Node.js,Express,SQLite

  • 前端: HTML,Bootstrap(用于布局),Chart.js(用于图表),Flatpickr(日期选择器),DataTables(用于表格数据)

  • 如下所示的文件夹结构。

my-dashboard/
├── client/
│   ├── index.html
│   ├── style.css
│   └── script.js
└── server/
    └── server.js

我向您展示了如何通过代码创建并填充一个 SQLite 数据库,我们可以将其用作仪表板的源数据。我们还讨论了环境设置以及前端和后端开发过程,并简要介绍了我们的数据仪表板功能。

最后,我详细介绍了我们需要创建的四个代码文件,并展示了如何在浏览器中运行仪表板。

posted @ 2026-03-28 09:50  布客飞龙II  阅读(49)  评论(0)    收藏  举报