使用 Cube.js 的 D3 仪表板教程

2025-06-07

使用 Cube.js 的 D3 仪表板教程

在本教程中,我将介绍如何使用Cube.js和最流行的数据可视化库D3.js构建一个基本的仪表板应用程序。虽然 Cube.js 本身不提供可视化层,但它很容易与任何现有的图表库集成。此外,您还可以使用Cube.js 模板,使用您最喜欢的图表库、前端框架和 UI 工具包搭建一个前端应用程序。脚手架引擎会将所有这些连接在一起,并将其配置为与 Cube.js 后端协同工作。

您可以在此处查看此仪表板的在线演示示例应用程序的完整源代码可在 Github 上找到

我们将使用 Postgres 来存储数据。Cube.js 将连接到 Postgres,并充当数据库和客户端之间的中间件,提供 API、抽象、缓存等功能。在前端,我们将使用 React、Material UI 和 D3 来渲染图表。您可以在下面找到示例应用程序的完整架构图。

如果您在阅读本指南时有任何疑问,请随时加入此Slack 社区并在那里发布您的问题。

祝你黑客愉快!💻

设置数据库和Cube.js

我们首先需要一个数据库。本教程将使用 Postgres。当然,您也可以使用自己喜欢的 SQL(或 Mongo)数据库。请参阅Cube.js 文档,了解如何连接到不同的数据库

如果您没有仪表板的任何数据,您可以加载我们的示例电子商务 Postgres 数据集。

$ curl http://cube.dev/downloads/ecom-dump-d3-example.sql > ecom-dump.sql
$ createdb ecom
$ psql --dbname ecom -f ecom-dump.sql
Enter fullscreen mode Exit fullscreen mode

现在,数据库中有了数据,我们就可以创建 Cube.js 后端服务了。在终端中运行以下命令:

$ npm install -g cubejs-cli
$ cubejs create d3-dashboard -d postgres
Enter fullscreen mode Exit fullscreen mode

上述命令安装 Cube.js CLI 并创建一个新服务,配置为与 Postgres 数据库一起使用。

Cube.js 使用环境变量进行配置。它使用以 开头的环境变量CUBEJS_。要配置与数据库的连接,我们需要指定数据库类型和名称。在 Cube.js 项目文件夹中,将 .env 的内容替换为以下内容:

CUBEJS_API_SECRET=SECRET
CUBEJS_DB_TYPE=postgres
CUBEJS_DB_NAME=ecom
CUBEJS_WEB_SOCKETS=true
Enter fullscreen mode Exit fullscreen mode

现在让我们启动服务器并在http://localhost:4000打开开发者游乐场。

$ npm run dev
Enter fullscreen mode Exit fullscreen mode

下一步是创建Cube.js 数据模式。Cube.js 使用数据模式生成 SQL 代码,该代码将在数据库中执行。Cube.js Playground 可以根据数据库表生成简单的模式。让我们导航到 Schema 页面,并生成仪表板所需的模式。选择line_itemsordersproductsproduct_categoriesusers表,然后单击“生成 Schema ”

让我们测试一下新生成的模式。前往“构建”页面,在下拉菜单中选择一个度量。您应该能够看到一个简单的折线图。您可以从图表库下拉菜单中选择 D3,查看 D3 可视化的示例。请注意,这只是一个示例,您可以随时对其进行自定义和扩展。

现在,让我们对架构进行一些更新。架构生成功能可以轻松启动和测试数据集,但对于实际用例,我们几乎总是需要手动进行更改。

在模式中,我们定义了度量和维度,以及它们如何映射到 SQL 查询中。您可以在此处找到有关数据模式的详尽文档。我们将向priceRange订单多维数据集添加一个维度。它将指示订单总价是否属于以下某个区间:“0 - 100 美元”、“100 - 200 美元”、“200 美元以上”。

为此,我们首先需要price为订单定义一个维度。我们的数据库中orders没有价格列,但我们可以根据line_items订单内的总价来计算价格。我们的模式已经自动指示并定义了与多维数据集之间的关系OrdersLineTimes您可以在此处阅读有关连接的更多信息。

// You can check the belongsTo join
// to the Orders cube inside the LineItems cube
joins: {
  Orders: {
    sql: `${CUBE}.order_id = ${Orders}.id`,
    relationship: `belongsTo`
  }
}
Enter fullscreen mode Exit fullscreen mode

LineItems多维数据集包含price一个具有特定类型的度量sum。我们可以从多维数据集中引用此度量Orders作为维度,它会返回属于该订单的所有订单项的总和。这被称为subQuery维度;您可以点击此处了解更多信息

// Add the following dimension to the Orders cube
price: {
  sql: `${LineItems.price}`,
  subQuery: true,
  type: `number`,
  format: `currency`
}
Enter fullscreen mode Exit fullscreen mode

现在,我们可以基于这个维度创建一个priceRange维度。我们将使用case 语句来定义价格区间的条件逻辑。

// Add the following dimension to the Orders cube
priceRange: {
  type: `string`,
  case: {
    when: [
      { sql: `${price} < 101`, label: `$0 - $100` },
      { sql: `${price} < 201`, label: `$100 - $200` }
    ],
    else: {
      label: `$200+`
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

让我们尝试一下我们新创建的维度!前往 Playground 中的“构建”页面,选择“订单数量”度量和“订单价格范围”维度。您可以随时通过点击控制栏上的SQL按钮来检查生成的 SQL。

这就是后端的全部内容!在下一部分中,我们将进一步了解如何使用 D3 渲染查询结果。

使用 D3.js 渲染图表

现在,我们可以构建第一个图表了,让我们检查一下 Playground 使用 D3 渲染它的示例代码。在此之前,我们需要了解 Cube.js 如何接受和处理查询并返回结果。

Cube.js 查询是一个简单的 JSON 对象,包含多个属性。查询的主要属性包括measuresdimensionstimeDimensionsfilters。您可以在此处 了解更多关于 Cube.js JSON 查询格式及其属性的信息。您可以随时在 Playground 中通过点击图表选择器旁边的“JSON查询”按钮来检查 JSON查询。

Cube.js 后端接受此查询,然后使用它和我们之前创建的模式来生成 SQL 查询。此 SQL 查询将在我们的数据库中执行,并将结果发送回客户端。

虽然可以通过普通的 HTTP REST API 查询 Cube.js,但我们将使用 Cube.js JavaScript 客户端库。它提供了一些实用的工具来处理从后端返回的数据。

数据加载完成后,Cube.js 客户端会创建一个ResultSet对象,该对象提供一组访问和操作数据的方法。我们现在将使用其中两个:ResultSet.series和。您可以在文档中ResultSet.chartPivot了解 Cube.js 客户端库的所有功能

ResultSet.series方法返回一个包含键、标题和系列数据的数据系列数组。该方法接受一个参数—— pivotConfig。它是一个对象,包含有关如何旋转数据的规则;我们稍后会详细讨论它。在折线图中,每个系列通常用一条单独的线表示。此方法对于以 D3 所需的格式准备数据非常有用。

// For query
{
  measures: ['Stories.count'],
  timeDimensions: [{
    dimension: 'Stories.time',
    dateRange: ['2015-01-01', '2015-12-31'],
    granularity: 'month'
  }]
}

// ResultSet.series() will return
[
  {
    "key":"Stories.count",
    "title": "Stories Count",
    "series": [
      { "x":"2015-01-01T00:00:00", "value": 27120 },
      { "x":"2015-02-01T00:00:00", "value": 25861 },
      { "x": "2015-03-01T00:00:00", "value": 29661 },
      //...
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

我们需要的下一个方法是ResultSet.chartPivot。它接受相同的pivotConfig参数并返回一个数据数组,其中包含 X 轴和我们拥有的每个系列的值。

// For query
{
  measures: ['Stories.count'],
  timeDimensions: [{
    dimension: 'Stories.time',
    dateRange: ['2015-01-01', '2015-12-31'],
    granularity: 'month'
  }]
}

// ResultSet.chartPivot() will return
[
  { "x":"2015-01-01T00:00:00", "Stories.count": 27120 },
  { "x":"2015-02-01T00:00:00", "Stories.count": 25861 },
  { "x": "2015-03-01T00:00:00", "Stories.count": 29661 },
  //...
]
Enter fullscreen mode Exit fullscreen mode

如上所述,pivotConfig参数是一个用于控制如何转换(或旋转)数据的对象。该对象具有两个属性:xy,它们都是数组。通过向其中一个属性添加度量或维度,您可以控制哪些内容显示在 X 轴上,哪些内容显示在 Y 轴上。对于包含measure和 的查询timeDimensionpivotConfig具有以下默认值:

{
   x: `CubeName.myTimeDimension.granularity`,
   y: `measures`
}
Enter fullscreen mode Exit fullscreen mode

这里,“measures”是一个特殊值,表示所有度量值都应该位于 Y 轴上。大多数情况下,默认值pivotConfig就可以了。在下一部分中,我将向您展示何时以及如何更改它。

现在,让我们看看选择 D3 图表后 Playground 生成的前端代码。在 Playground 中选择一个度量,并将可视化类型更改为 D3。接下来,点击“代码”按钮,查看渲染图表所需的前端代码。

这是该页面的完整源代码。

import React from 'react';
import cubejs from '@cubejs-client/core';
import { QueryRenderer } from '@cubejs-client/react';
import { Spin } from 'antd';

import * as d3 from 'd3';
const COLORS_SERIES = ['#FF6492', '#141446', '#7A77FF'];

const draw = (node, resultSet, chartType) => {
  // Set the dimensions and margins of the graph
  const margin = {top: 10, right: 30, bottom: 30, left: 60},
    width = node.clientWidth - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;

  d3.select(node).html("");
  const svg = d3.select(node)
  .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

  // Prepare data in D3 format
  const data = resultSet.series().map((series) => ({
    key: series.title, values: series.series
  }));

  // color palette
  const color = d3.scaleOrdinal()
    .domain(data.map(d => d.key ))
    .range(COLORS_SERIES)

  // Add X axis
  const x = d3.scaleTime()
    .domain(d3.extent(resultSet.chartPivot(), c => d3.isoParse(c.x)))
    .range([ 0, width ]);
  svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x));

  // Add Y axis
  const y = d3.scaleLinear()
    .domain([0, d3.max(data.map((s) => d3.max(s.values, (i) => i.value)))])
    .range([ height, 0 ]);
  svg.append("g")
    .call(d3.axisLeft(y));

  // Draw the lines
  svg.selectAll(".line")
    .data(data)
    .enter()
    .append("path")
      .attr("fill", "none")
      .attr("stroke", d => color(d.key))
      .attr("stroke-width", 1.5)
      .attr("d", (d) => {
        return d3.line()
          .x(d => x(d3.isoParse(d.x)))
          .y(d => y(+d.value))
          (d.values)
      })

}

const lineRender = ({ resultSet }) => (
  <div ref={el => el && draw(el, resultSet, 'line')} />
)


const API_URL = "http://localhost:4000"; // change to your actual endpoint

const cubejsApi = cubejs(
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1NzkwMjU0ODcsImV4cCI6MTU3OTExMTg4N30.nUyJ4AEsNk9ks9C8OwGPCHrcTXyJtqJxm02df7RGnQU",
  { apiUrl: API_URL + "/cubejs-api/v1" }
);

const renderChart = (Component) => ({ resultSet, error }) => (
  (resultSet && <Component resultSet={resultSet} />) ||
  (error && error.toString()) ||
  (<Spin />)
)

const ChartRenderer = () => <QueryRenderer
  query={{
    "measures": [
      "Orders.count"
    ],
    "timeDimensions": [
      {
        "dimension": "Orders.createdAt",
        "granularity": "month"
      }
    ],
    "filters": []
  }}
  cubejsApi={cubejsApi}
  render={renderChart(lineRender)}
/>;

export default ChartRenderer;
Enter fullscreen mode Exit fullscreen mode

呈现图表的 React 组件只是一行包装draw函数,它完成整个工作。

const lineRender = ({ resultSet }) => (
  <div ref={el => el && draw(el, resultSet, 'line')} />
)
Enter fullscreen mode Exit fullscreen mode

这个函数包含很多功能draw。虽然它已经渲染了一个图表,但可以将其视为一个示例,并作为自定义的良好起点。由于我们将在下一部分中创建自己的仪表板,因此我将向您展示如何操作。

请随意单击“编辑”按钮并使用代码沙箱中的代码进行操作。

构建前端仪表板

现在我们准备构建前端应用程序了。我们将使用 Cube.js 模板,它是一个脚手架引擎,用于快速创建配置为与 Cube.js 后端配合使用的前端应用程序。它提供了多种不同的前端框架、UI 工具包和图表库供您选择。我们将选择 React、Material UI 和 D3.js。让我们导航到“仪表板应用”选项卡并创建一个新的仪表板应用程序。

生成应用程序并安装所有依赖项可能需要几分钟时间。完成后,dashboard-appCube.js 项目文件夹中会有一个文件夹。要启动前端应用程序,请前往 Playground 中的“Dashboard App”选项卡并点击“Start”按钮,或者在 dashboard-app 文件夹中运行以下命令:

$ npm start
Enter fullscreen mode Exit fullscreen mode

确保 Cube.js 后端进程已启动并运行,因为我们的前端应用程序使用了它的 API。前端应用程序在http://localhost:3000上运行。如果你在浏览器中打开它,你应该能够看到一个空的仪表板。

要将图表添加到仪表板,我们可以在 Playground 中构建图表并点击“添加到仪表板”按钮,也可以编辑文件夹src/pages/DashboardPage.js中的文件dashboard-app。我们选择后者。此外,该文件还声明了一个DashboardItems变量,该变量是图表查询的数组。

编辑dashboard-app/src/pages/DashboardPage.js以将图表添加到仪表板。

-const DashboardItems = [];
+const DashboardItems = [
+  {
+    id: 0,
+    name: "Orders last 14 days",
+    vizState: {
+      query: {
+        measures: ["Orders.count"],
+        timeDimensions: [
+          {
+            dimension: "Orders.createdAt",
+            granularity: "day",
+            dateRange: "last 14 days"
+          }
+        ],
+        filters: []
+      },
+      chartType: "line"
+    }
+  },
+  {
+    id: 1,
+    name: "Orders Status by Customers City",
+    vizState: {
+      query: {
+        measures: ["Orders.count"],
+        dimensions: ["Users.city", "Orders.status"],
+        timeDimensions: [
+          {
+            dimension: "Orders.createdAt",
+            dateRange: "last year"
+          }
+        ]
+      },
+      chartType: "bar",
+      pivotConfig: {
+        x: ["Users.city"],
+        y: ["Orders.status", "measures"]
+      }
+    }
+  },
+  {
+    id: 3,
+    name: "Orders by Product Categories Over Time",
+    vizState: {
+      query: {
+        measures: ["Orders.count"],
+        timeDimensions: [
+          {
+            dimension: "Orders.createdAt",
+            granularity: "month",
+            dateRange: "last year"
+          }
+        ],
+        dimensions: ["ProductCategories.name"]
+      },
+      chartType: "area"
+    }
+  },
+  {
+    id: 3,
+    name: "Orders by Price Range",
+    vizState: {
+      query: {
+        measures: ["Orders.count"],
+        filters: [
+          {
+            "dimension": "Orders.price",
+            "operator": "set"
+          }
+        ],
+        dimensions: ["Orders.priceRange"]
+      },
+      chartType: "pie"
+    }
+  }
+];
Enter fullscreen mode Exit fullscreen mode

正如您上面看到的,我们刚刚添加了一个 Cube.js 查询对象数组。

如果您刷新仪表板,您应该能够看到您的图表!

您会注意到我们的一个查询具有pivotConfig如下定义。

  pivotConfig: {
    x: ["Users.city"],
    y: ["Orders.status", "measures"]
  }
Enter fullscreen mode Exit fullscreen mode

正如我在上一部分中提到的,默认值pivotConfig通常可以正常工作,但在某些情况下,例如本例,我们需要对其进行调整才能获得所需的结果。我们想在这里绘制一个条形图,X 轴表示城市,Y 轴表示订单数量,并按订单状态分组。这正是我们在 中传递的内容pivotConfigUsers.cityX 轴表示度量值,Orders.statusY 轴表示度量值,以获得分组结果。

要自定义图表的渲染,您可以编辑该dashboard-app/src/pages/ChartRenderer.js文件。它看起来应该与我们在上一节中看到的类似。

您可以在此处查看此仪表板的在线演示示例应用程序的完整源代码可在 Github 上找到

恭喜你完成本指南!🎉

我很乐意听听您使用本指南的体验。如果您有任何意见或反馈,请在此处的评论区或Slack 社区留言。谢谢!希望本指南对您有所帮助!

文章来源:https://dev.to/keydunov/d3-dashboard-tutorial-with-cube-js-ehb
PREV
React Dashboard 终极指南。第一部分:概述和分析后端
NEXT
编程技能的四个层次