Cube.js Blog

Tableau-like Charts with MuzeJS, Cube.js, and React

Author avatarNakshatra MukhopadhyayJanuary 13, 2021TutorialsVisualizations
Tableau-like Charts with MuzeJS, Cube.js, and React
Show Original

In this blog post, we'll walk through the steps to create a Tableau-like data visualization using Muze, a free JavaScript charting library powered by WebAssembly, for the front-end and Cube.js for the analytical API.

We thank Nakshatra Mukhopadhyay, a software engineer at Charts.com, for contributing this blog post.

A. Introduction

In the spectrum of JavaScript data visualization libraries, most libraries position themselves at one of two extremes: monolithic charting libraries (e.g., Highcharts, Chart.js, or amCharts) or modular, low level libraries (e.g., D3.js).

The charting libraries are very easy to get started with but do not provide a lot of control over their behavior in advanced use cases. On the other hand, D3.js provides incredibly fine grained control over every aspect of its behavior but that comes at the cost of a steeper learning curve and a lot of glue code to bridge all of its modules together into a cohesive visualization system. And neither of these two approaches have much knowledge about the structure and attributes of the data being fed to them without significant implementation effort.

MuzeJS by Charts.com is a free JavaScript library which makes it easy to create Tableau-like charts in the browser. It bridges the gap between the previous two approaches to data visualization by taking a data-first approach. Using the table of data provided to it, it can make intelligent inferences about the data to better create a cross-interactive visualization system that has powerful slicing and dicing capabilities, just like Tableau—but in the browser.

Incidentally, this approach also fits well with Cube.js' paradigms. In this blog post, we're going to build a data-driven chart in React using Cube.js and MuzeJS, with data stored in a PostgreSQL database. We'll let Cube.js interact with our database, shave off most of the data and send the required subset of it to the front-end. Once the data is on the front-end, we'll use MuzeJS and its blazing fast DataModel to visualize it.

Here's a peek at what the end result will be!

Looks daunting, right? Don't worry! With Cube.js and MuzeJS generating this visualization is easy. Installing and setting everything up is honestly what's gonna take longer! 😅

Let's get crackin'!

B. Pre-requisites

  1. Familiarity with programming concepts, Ubuntu, the UNIX terminal, SQL, JavaScript, React and npm is assumed for the purposes of this tutorial.
  2. Working installations of Ubuntu ≥ v16.04, NodeJS ≥ v10.13.0 and npm ≥ v6.4.1

C. Installing PostgreSQL and

First things first—let's get our database set up. In this example we'll be using PostgreSQL.

Cube.js can connect with a lot of databases other than PostgreSQL. You can check out the full list here.

Fire up the terminal. At the prompt, we are going to install PostgreSQL 12 by running the following command:

sudo apt install postgresql-12

Provide the user's password, if asked, and wait for the installation to complete. The installer will automatically create a Ubuntu system user called postgres as well as a PostgreSQL superuser called postgres with all necessary permissions to work with PostgreSQL databases.

The installer will also have started a PostgreSQL server. You can see the status of the server at any time using the command pg_lsclusters. If, in the output of pg_lsclusters, the server's "status" is "down", you can start the server using sudo pg_ctlcluster 12 main start (you may be prompted for your password here as well).

Now, we'll be creating a PostgreSQL Role against our Ubuntu username.

  1. Note down the name of the current system user that we're using Ubuntu as.
whoami

This command will output our username. We are going to need this information in just a bit.

  1. Switch over to the postgres system user (which was created by the PostgreSQL installer).
sudo -i -u postgres

After asking for your user's password, the shell prompt will change to indicate you're logged in as postgres in postgres' home directory.

  1. Start the PostgreSQL shell.
psql

The terminal prompt will change to show that you're now in the PostgreSQL shell.

  1. Create a PostgreSQL Role with the username that we got with the whoami command.

Replace <whoami_result> with the Ubuntu system username and replace <password_here> with your desired password.

CREATE ROLE <whoami_result> WITH CREATEDB LOGIN PASSWORD '<password_here>';

This will have created a PostgreSQL Role with the name of the Ubuntu system user's name and given it permission to create databases and log into PostgreSQL with the password chosen.

  1. Quit the PostgreSQL shell.
\q

You'll now be in the shell of the postgres system user.

  1. Log out of the postgres system user's shell.
exit

You're now back in the original Ubuntu user's shell.

  1. Create a PostgreSQL database against your username.
createdb

Now, you will be able to connect to PostgreSQL with your Ubuntu user instead of having to switch over to the postgres user every time.

The next thing to do is loading a sample dataset.

In this example we are going to use Cube.js' sample e-commerce dataset found here.

  1. Download the dataset and save it in a file named ecom-dump.sql in the home directory.
curl https://cube.dev/downloads/ecom-dump.sql > ecom-dump.sql

The above command will download the sample dataset from Cube.js' servers and save as ecom-dump.sql. After the download has completed you can check whether the file has been created using the ls command.

  1. Load the ecom-dump.sql file into PostgreSQL to create the database.
createdb ecom && psql --dbname ecom -f ecom-dump.sql

This will have created a database named ecom in PostgreSQL from the ecom-dump.sql file.

  1. Start the PostgreSQL shell.
psql

The terminal prompt will change to show that you're now in the PostgreSQL shell.

  1. Let's quickly check whether the ecom database has been created successfully.
\l

That will output a table onto the terminal where we should be able to see a database with its "Name" as ecom.

  1. Quit the PostgreSQL shell.
\q

So, now let's take a quick tour of the Cube.js e-commerce dataset.

  1. Start the PostgreSQL shell.
psql
  1. Connect to the ecom database.
\c ecom
  1. List the tables in the database.
\dt

It should show the list of tables in the ecom database: line_items, orders, product_categories, products, suppliers and users.

For the purposes on this walk-through, we'll be focusing on four of these tables: users, orders, products and product_categories.

  1. Take a look at the users table...
SELECT * FROM users LIMIT 1;
id |   city   | age |    company    | gender |     created_at      | first_name | last_name 
----+----------+-----+---------------+--------+---------------------+------------+-----------
 16 | New York |  52 | A Arcu Sed PC | male   | 2018-06-06 23:27:14 | Walker     | Cronin

...then the orders table...

SELECT * FROM orders LIMIT 1;
id | user_id | number |   status   |    completed_at     |     created_at      | product_id 
----+---------+--------+------------+---------------------+---------------------+------------
  1 |     371 |     97 | processing | 2019-01-09 00:00:00 | 2019-01-02 00:00:00 |         85

...the products table...

SELECT * FROM products LIMIT 1;
id |        name         |          description          |     created_at      | supplier_id | product_category_id 
----+---------------------+-------------------------------+---------------------+-------------+---------------------
  1 | Tasty Rubber Gloves | Tools Incredible Wooden Pizza | 2021-07-05 00:00:00 |          99 |                   9

And, finally, the product_categories table.

SELECT * FROM product_categories LIMIT 1;
id |     created_at      |   name   
----+---------------------+----------
  1 | 2016-12-25 06:50:22 | Clothing

With the structures of those tables in out minds, lets plan out what we want to visualize with MuzeJS.

  1. Quit the PostgreSQL shell.
\q

D. Planning the Task Ahead

Let's imagine that we want to visualize the split between the quantity of orders placed by our male and female customers across all products categories in every city for the year 2019.

Let's break that statement down. We'll refer to the tables we printed above to help us plan.

We can figure out the quantity of orders by counting the entries in our orders table. Its user_id column allows us to refer to the users table by id from where we can find information about the gender and city of the user who placed the order. The orders table also has a product_id column which maps to the id column in the products table. The products table, in turn, has a product_category_id column which maps to the id column in the product_categories table. Thus, the "join path" for our use case looks like the following.

Screenshot 2021-01-13 at 22.10.16.png

Fortunately, the Cube.js CLI can analyze our database structure and create simple join paths for us automatically! We simply make our queries using Cube.js indicating what results we want and it'll will take care of generating efficient SQL statements to query our ecom PostgreSQL database across all necessary tables and return the result.

Once the result is ready, we'll use a small helper function to extract some meta information about the results. We'll provide this information along with the result table to MuzeJS and ask it to plot the charts. And bada-bing bada-boom! We'll have our visualization ready. Easy!

E. Understanding Measures and Dimensions

Before Cube.js can communicate with our database and before MuzeJS can visualize our data, they first need information about the columns of our tables. Cube.js calls this information cubes which are an essential part of the data schema. MuzeJS also calls them the schema. At the least, both Cube.js and MuzeJS need to know which of the fields in the tables are measures and which are dimensions.

Measure is a term used to refer to quantitative data. In our examples, the number of products sold is a measure. Other examples of measures could be the average temperatures of a region, the maximum price of a stock, etc. As is evident from their naming, they can be "measured" by using some instrument and can have mathematical functions applied on them to summarize them, such as average, mean, max, etc.

Dimension is used to refer to categorical data; such as the gender or the city of a customer, the name of a product, etc. These are generally distinct in nature and can't usually be measured by instruments. They also cannot be summarized using mathematical functions. Instead, they serve to categorize the measures that we have recorded.

Other than the usual kinds of dimensions like city names or product categories, there is also a special type of dimension—the dimension of time, often referred to as the temporal dimension. Time needs special treatment because we can treat it as distinct units, like January is clearly distinct from June. But at the same time, it is also continuous in nature: 2020 can't come before 1980 for example.

F. Analytical API with Cube.js

Cube.js will help us scaffold out our project's analytical API and will be useful for generating authentication tokens to use with MuzeJS. Let's go get that npm install-ed now.

Use npm to install the cubejs-cli package globally on your system.

npm install -g cubejs-cli

After the installation is complete the cubejs command should be available to the system. Run cubejs -V to double-check. The version of the Cube.js CLI that was installed should be output to the console.

G. Preparing the Analytical API

  1. Navigate to a directory which will contain our project and scaffold out a back-end for our project using the Cube.js.
npx cubejs-cli create dashboard-backend -d postgres

This will create a npm project in a directory called dashboard-backend in the location where the above command was run. It will have the the Cube.js server installed and configured to use PostgreSQL as the database.

  1. cd into the dashboard-backend directory.
cd dashboard-backend
  1. Replace the contents of the .env file with the following block. Replace <USERNAME_IN_C.II.4> and <PASSWORD_IN_C.II.4> with the database username and password created earlier in this article. The CUBEJS_API_SECRET will not be of particular use during this tutorial and during development. You can set it to be any string you want. But, in production you should ensure that the values are properly set and used to generate your authentication tokens. More information about Cube.js security can be found here.
# dashboard-backend/.env

CUBEJS_DEV_MODE=false
CUBEJS_DB_TYPE=postgres
CUBEJS_DB_NAME=ecom
CUBEJS_DB_USER=<USERNAME_IN_C.II.4>
CUBEJS_DB_PASS=<PASSWORD_IN_C.II.4>
CUBEJS_API_SECRET=<SOME_SUPER_SECRET>

Pro tip: never commit your .env files to the version control. They are meant to contain machine and user specific secrets which can be dangerous in the wrong hands.

Cube.js will use the values defined here when communicating with our database and our front-end.

  1. Generate the cubes for the database tables that we are interested in: orders, users, products and product_categories.
cubejs generate -t users,orders,products,product_categories

Provided that our database is running in the background and our .env file contents are correct, Cube.js will generate the cubes for our tables in the schema directory within the dashboard-backend project. You can open up the files in the schema directory and check out the generated cubes. In the future, as you get more familiar with Cube.js you will be able to modify the contents of these files as you need. For this walk-through, however, the generated cubes will be sufficient. You can find more information about the Cube.js schema here.

  1. Start the Cube.js development server. When generating the dashboard-backend project, the Cube.js CLI also helpfully created a package.json file with the script to run our development server on http://localhost:4000.
npm run dev

Our front-end will be sending the queries for data to the Cube.js server at http://localhost:4000.

  1. Initialize the Cube.js Playground by visiting http://localhost:4000 in your browser. While we will not be using the playground much in this article, you'll likely need to get familiar with it as you explore Cube.js on your own. You can find more information about the Cube.js playground in the Cube.js documentation pages.
  2. We'll keep the Cube.js development server running in the current terminal. OPEN UP A NEW TERMINAL before proceeding with the next steps.

H. Preparing the Frontend

We'll begin by installing all our dependencies and then move to creating our visualization.

  1. Navigate to the directory which contains our dashboard-backend project. Just to be clear—do NOT go into the dashboard-backend directory. Our front-end will be located as a sibling to dashboard-backend in our filesystem.
  2. Create a new React project using create-react-app.
npx create-react-app --use-npm dashboard-frontend
  1. cd into the dashboard-frontend directory.
cd dashboard-frontend

Now we can install the Cube.js Client.

  1. Install the Cube.js client and the React bindings for Cube.js.
npm install --save @cubejs-client/core @cubejs-client/react
  1. Create a .env file in dashboard-frontend with the following content.
# dashboard-frontend/.env

REACT_APP_CUBEJS_TOKEN=<TOKEN_GENERATED_FROM_CUBEJS_USING_CUBEJS_API_SECRET>
REACT_APP_API_URL=http://localhost:4000/cubejs-api/v1

Again: never commit your .env files to version control. They are meant to contain machine and user specific secrets which can be dangerous in the wrong hands.

Remember we started the Cube.js server at http://localhost:4000 back in Section G, Step 5? In development mode, Cube.js makes its query endpoint available there under /cubejs-api/v1. When deploying to production you should change this URL to the location where the Cube.js server is running. More information about deploying Cube.js can be found here. We'll use the REACT_APP_API_URL when making queries for our data.

Similar to the CUBEJS_API_SECRET, the value of REACT_APP_CUBEJS_TOKEN is not important during development. You can set it to be any string you want. However, in production you should generate your tokens using the CUBEJS_API_SECRET. More information about security, tokens and secrets are in the Cube.js docs here.

Now it's time to install MuzeJS!

We'll follow the installation instructions as in the React-Muze installation documentation.

  1. Install MuzeJS and the React-Muze.
npm install --save @chartshq/muze @chartshq/react-muze
  1. Install Copy Webpack Plugin.
npm install --save-dev copy-webpack-plugin

We'll use this Webpack plugin to copy over MuzeJS' assets during compilation.

  1. Install React App Rewired.
npm install --save-dev react-app-rewired

Create React App does not provide direct access to the underlying Webpack configuration. Instead, we'll use React App Rewired package to use the Copy Webpack Plugin with Create React App.

  1. Create a new file called config-overrides.js inside the dashboard-frontend directory, at its root. Populate it with the following code.
// dashboard-frontend/config-overrides.js

const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = (config) => {
  if (!config.plugins) config.plugins = [];

  config.plugins.push(
    new CopyPlugin({
      patterns: [{ from: path.resolve('node_modules', '@chartshq/muze/dist') }],
    })
  );

  return config;
};

The exported function is very simple. If a plugins array is not already present in Webpack's config, we create it. Then we tell Copy Webpack Plugin to copy everything from MuzeJS' dist directory. We push the Copy Webpack Plugin into the plugins array and return the modified Webpack configuration.

  1. In dashboard-frontend's package.json file's start, build and test scripts, use React App Rewired instead of React Scripts. DO NOT change the eject script to use React App Rewired.
{
  "name": "dashboard-frontend",
  ...
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  ...
}

React App Rewired will now use the function exported from config-overrides.js when running, building and testing our React application.

I. Visualizing the Data with React and MuzeJS

Whew! Alrighty! Everything's installed and set up and prepared. Let's get to visualizing our data. Reminder: we want to visualize the split between the quantity of orders placed every month by our male and female customers across all products categories in every city for the year 2019.

Open up the file dashboard-frontend/src/App.js and delete its contents. We'll be starting from a clean slate.

Now we can initialize Cube.js:

  1. Extract the values we put in the .env file in Section H, Part II, Step 2 from process.env.
// dashboard-frontend/src/App.js

const { REACT_APP_CUBEJS_TOKEN, REACT_APP_API_URL } = process.env;

Create React App, very helpfully, has injected the values we provided in the .env file in the global process.env.

Create React App only does this injection for the keys in the .env file which begin with "REACT_APP_". Other values in the .env file are not injected.

  1. Import cubejs from @cubejs-client/core and initialize the cubejsApi with the Cube.js token and API URL.
// dashboard-frontend/src/App.js

import cubejs from '@cubejs-client/core';

const cubejsApi = cubejs(REACT_APP_CUBEJS_TOKEN, { apiUrl: REACT_APP_API_URL });

We will use this cubejsApi to make queries against the Cube.js back-end.

  1. Prepare the query for Cube.js. We'll start simple and see what the query for the total number of orders that have ever been placed looks like.

Recall our discussion about measures and dimensions. Here, the number of orders is, of course, a quantitative value and hence, a measure. Since we can query multiple measures in a since object, Cube.js accepts an array of measures in its query object. The format of every measure is a string in the form <TABLE_NAME>.<FIELD_NAME>. Our table name is Orders and field name is count.

You won't find the count field directly in your database tables, but if you check out the schema in the dashboard-backend/schema/Orders.js file generated by Cube.js in Section G, Step 4, you'll find that a count field is defined in the measures section of the schema. You can find more information about the Cube.js schema in the Cube.js docs here.

// dashboard-frontend/src/App.js

const CUBE_QUERY = {
  measures: ['Orders.count'],
};

Let's refine the query so that it includes results for every month of the year 2019.

For time fields, Cube.js asks for all the information in a property called timeDimensions in its query. Within it, we provide dimension name, the date range and the granularity of the result we need. The granularity is important here because our data in the database contains details for every single day; but in our visualization, we are going to show data aggregated into months.

// dashboard-frontend/src/App.js

const CUBE_QUERY = {
  measures: ['Orders.count'],
  timeDimensions: [
    {
      dimension: 'Orders.createdAt',
      dateRange: ['2019-01-01', '2019-12-31'],
      granularity: 'month',
    },
  ],
};

Now, we'll add our dimension fields to our query. We need the gender of our users, the city they are from and the product category that they have ordered from.

// dashboard-frontend/src/App.js

const CUBE_QUERY = {
  measures: ['Orders.count'],
  dimensions: ['ProductCategories.name', 'Users.gender', 'Users.city'],
  timeDimensions: [
    {
      dimension: 'Orders.createdAt',
      dateRange: ['2019-01-01', '2019-12-31'],
      granularity: 'month',
    },
  ],
};

And voila! Our Cube.js query is ready!

Now we can create our React Component.

  1. Create and export the React Function Component from the dashboard-frontend/src/App.js file.
// dashboard-frontend/src/App.js

import React from 'react';

export default function App() {
  return <></>;
}

We'll return an empty React Fragment for now. We'll replace it with the MuzeJS' React component soon.

  1. Import and use Cube.js' React hook for making queries inside out React component. We'll use the CUBE_QUERY we've already created.
// dashboard-frontend/src/App.js

import { useCubeQuery } from '@cubejs-client/react';

// ...

export default function App() {
  // ...
  const { resultSet } = useCubeQuery(CUBE_QUERY, { cubejsApi });
  // ...
}

The resultSet returned from useCubeQuery provides several methods to interact with the resultant data. We'll primarily be using two methods available on resultSet: pivotTable and annotation. The pivotTable method returns the fetched data as a flat JSON object. We can directly pass it to MuzeJS. The annotation method returns some meta-information about the resulting data. We'll use this to create the schema for MuzeJS.

  1. Since MuzeJS is a table driven visualization library, we'll get its data table ready first. MuzeJS provides a entity called a DataModel as its data source. This DataModel requires two things to initialize it with: a data and a schema. The data is received straight from Cube.js via the pivotTable method on the resultSet. For the schema, we'll use the meta-information returned by the annotation method on resultSet. Let's examine the return value of the annotation method.
{
  "measures": {
    "Orders.count": {
      "title": "Orders Count",
      // ...
    }
  },
  "dimensions": {
    "ProductCategories.name": {
      "title": "Product Categories Name",
      // ...
    },
  },
  "timeDimensions": {
    "Orders.createdAt.month": {
      "title": "Orders Created at",
      // ...
    },
  }
}

Now, we want to convert the above object into an object of the form below.

[
  {
    "name": "Orders.createdAt.month", // Name of the column in the Cube.js resultSet
    "displayName": "Orders Created at", // The name used in the visualization for this column 
    "type": "dimension", // The kind of data this column represents: measure or dimension
    "subtype": "temporal", // Only needed for time based column. Set it to 'temporal' if the column is in the time dimension
    "format": "%Y-%m-%dT%H:%M:%S" // The format in which time is represented by Cube.js. For a complete reference to available time formats in DataModel [check here](https://muzejs.org/docs/wa/latest/datamodel/api-reference/dateformat).
  },
  {
    "name": "Orders.count",
    "displayName": "Orders Count",
    "type": "measure"
  },
  {
    "name": "ProductCategories.name",
    "displayName": "Product Categories Name",
    "type": "dimension"
  },
  // ...
]

Note that the DataModel's schema shares some of the same fundamental concepts with Cube.js' schema, such as measures, dimensions and time dimensions.

To convert the resultSet annotation object into a schema compatible with DataModel, we'll simply iterate over the measures, dimensions and timeDimensions, extract their names (e.g. "Orders.count") and titles (e.g. "Orders Count") and concatenate them into an array. Here's a function which shows one way of doing that.

// dashboard-frontend/src/App.js

// ...

function generateSchema({ dimensions, measures, timeDimensions }) {
  const muzeDimensions = Object.entries(dimensions).map(([name, { title: displayName }]) => ({
    name,
    displayName,
    type: 'dimension',
  }));

  const muzeMeasures = Object.entries(measures).map(([name, { title: displayName }]) => ({
    name,
    displayName,
    type: 'measure',
  }));

  const muzeTimeDimensions = Object.entries(timeDimensions).map(
    ([name, { title: displayName }]) => ({
      name,
      displayName,
      type: 'dimension',
      subtype: 'temporal',
      format: '%Y-%m-%dT%H:%M:%S',
    })
  );

  return muzeTimeDimensions.concat(muzeMeasures).concat(muzeDimensions);
}

// ...
  1. Initialize a state variable which will contain the DataModel instance once its ready. Initially, we'll let its value stay undefined.
// dashboard-frontend/src/App.js

import React, { useState } from 'react';

// ...
export default function App() {
  // ...
  const [dataModel, setDataModel] = useState();
  // ...
}
  1. Inside a useEffect hook, we'll instantiate the DataModel. For its data, we'll use the value of resultSet.tablePivot() and for the schema we'll use the return value of the generateSchema() function (defined in Step 3 just above) after we pass resultSet.annotation() to it.
// dashboard-frontend/src/App.js

import Muze from '@chartshq/react-muze/components';
import React, { useState, useEffect } from 'react';

// ...
export default function App() {
  // ...
  useEffect(() => {
    let dataModel;

    (async () => {
      if (resultSet != null) { // If Cube.js is ready with the resultSet
        const data = resultSet.tablePivot(); // Extract the data
        const schema = generateSchema(resultSet); // Prepare the schema

    const DataModel = await Muze.DataModel.onReady(); // Wait for the DataModel class to be ready
        const formattedData = await DataModel.loadData(data, schema); // Load the data and schema

        dataModel = new DataModel(formattedData); // Use the formatted data to create the DataModel instance

        setDataModel(dataModel); // Set the state variable to be the newly created DataModel instance
      }
    })();

    return () => dataModel != null && dataModel.dispose(); //  Clean up the dataModel instance if the component is disposed
  }, [resultSet]); // Make sure the dependencies to useEffect are declared!
  // ...
}
  1. Replace the empty React fragment returned in Step 1 above with MuzeJS' React component.
// dashboard-frontend/src/App.js

// ...

export default function App() {
  // ...

  return dataModel == null ? 'Loading data. Please wait.' : <Muze data={dataModel}></Muze>;
}

We've also added a loading message to our component which will be shown while Cube.js fetches the data and the DataModel parses it. This loading message can simply be replaced with some Spinner component if needed.

  1. Add a Canvas component as a child of the Muze component. This Canvas defines the various properties of our visualization, such as the width, height, the fields to be plotted across rows and columns, etc. As a child of the Canvas component, we specify the Layer with a mark prop which indicates the plot type with which to plot the data.
// dashboard-frontend/src/App.js

import Muze, { Canvas } from '@chartshq/react-muze/components';

// ...

export default function App() {
  // ...

  return dataModel == null ? (
    'Loading data. Please wait.'
  ) : (
    <Muze data={dataModel}>
      <Canvas
        width={'1440px'}
        height={'900px'}
        columns={['Orders.createdAt.month']}
        rows={['Orders.count']}
      >
        <Layer mark="bar"></Layer>
      </Canvas>
    </Muze>
  );
}

Here, we've defined a 1440px by 900px Canvas and plotted the values of Orders.createdAt.month on the columns, i.e. the X-Axis and Orders.count on the rows, i.e. the Y-Axis. The mark for the Layer on which the data will be plotted is set to be bar.

  1. Save the dashboard-frontend/src/App.js file and start the Create React App development server.
npm start

A webpage should open up in your system's default browser and show a visualization like the one below. Nice!

Screenshot_2020-11-10_React_App.png
  1. Here's where the real fun begins!

You'll notice that the chart we rendered simply shows us the number of orders placed every month in 2019. Let's visualize some more details! First, we'll color each bar with separate colors to indicate how much of the orders were by male customers as versus female customers! We'll add the colors prop to the Canvas component and set its value to be Users.gender.

// dashboard-frontend/src/App.js

// ...

export default function App() {
  // ...

  return dataModel == null ? (
    // ...
    <Muze data={dataModel}>
      <Canvas
        // ...
        color={'Users.gender'}
      >
        <Layer mark="bar"></Layer>
      </Canvas>
    </Muze>
  // ...
}

Save the file and you should see this in your browser.

Screenshot_2020-11-10_React_App(1).png
  1. Let's go deeper and slice and dice our chart! Add the ProductCategories.name field to the Canvas component's columns prop.
// dashboard-frontend/src/App.js

// ...

export default function App() {
  // ...

  return dataModel == null ? (
    // ...
    <Muze data={dataModel}>
      <Canvas
        // ...
        columns={['ProductCategories.name', 'Orders.createdAt.month']}
        // ...
      >
        <Layer mark="bar"></Layer>
      </Canvas>
    </Muze>
  // ...
}

Save the file and you should see the single chart now split into a number of columns where each column shows the monthly number of orders colored by gender for every product category!

Screenshot_2020-11-10_React_App(2).png
  1. Now lets slice some rows! Add the Users.city field to the Canvas component's rows prop.
// dashboard-frontend/src/App.js

// ...

export default function App() {
  // ...

  return dataModel == null ? (
    // ...
    <Muze data={dataModel}>
      <Canvas
        // ...
        rows={['Orders.count', 'Users.city']}
        // ...
      >
        <Layer mark="bar"></Layer>
      </Canvas>
    </Muze>
  // ...
}

Save the file and you should see the visualization below. We've finally achieved what we set out to do: visualize the split between the quantity of orders placed every month by our male and female customers across all products categories in every city for the year 2019!

Screenshot_2020-11-10_React_App(3).png

Go ahead! Click on the legend items! Click and drag to select some plots in one cell of the visualization! Everything is cross-connected!

The explanation above may have made it look like the code was too long. But its actually just about 90 lines of code (at a per line character limit of 80). Check it out below!

// dashboard-frontend/src/App.js

import Muze, { Canvas, Layer } from '@chartshq/react-muze/components';
import cubejs from '@cubejs-client/core';
import { useCubeQuery } from '@cubejs-client/react';
import React, { useEffect, useState } from 'react';

const { REACT_APP_CUBEJS_TOKEN, REACT_APP_API_URL } = process.env;

const CUBE_QUERY = {
  measures: ['Orders.count'],
  dimensions: ['ProductCategories.name', 'Users.gender', 'Users.city'],
  timeDimensions: [
    {
      dimension: 'Orders.createdAt',
      dateRange: ['2019-01-01', '2019-12-31'],
      granularity: 'month',
    },
  ],
};

const cubejsApi = cubejs(REACT_APP_CUBEJS_TOKEN, { apiUrl: REACT_APP_API_URL });

function generateSchema({ dimensions, measures, timeDimensions }) {
  const muzeDimensions = Object.entries(dimensions).map(
    ([name, { title: displayName }]) => ({
      name,
      displayName,
      type: 'dimension',
    })
  );

  const muzeMeasures = Object.entries(measures).map(
    ([name, { title: displayName }]) => ({
      name,
      displayName,
      type: 'measure',
    })
  );

  const muzeTimeDimensions = Object.entries(timeDimensions).map(
    ([name, { title: displayName }]) => ({
      name,
      displayName,
      type: 'dimension',
      subtype: 'temporal',
      format: '%Y-%m-%dT%H:%M:%S',
    })
  );

  return muzeTimeDimensions.concat(muzeMeasures).concat(muzeDimensions);
}

export default function App() {
  const [dataModel, setDataModel] = useState();
  const { resultSet } = useCubeQuery(CUBE_QUERY, { cubejsApi });

  useEffect(() => {
    let dataModel;

    (async () => {
      if (resultSet != null) {
        const data = resultSet.tablePivot();
        const schema = generateSchema(resultSet.annotation());

        const DataModel = await Muze.DataModel.onReady();
        const formattedData = await DataModel.loadData(data, schema);

        dataModel = new DataModel(formattedData);

        setDataModel(dataModel);
      }
    })();

    return () => dataModel != null && dataModel.dispose();
  }, [resultSet]);

  return dataModel == null ? (
    'Loading data. Please wait.'
  ) : (
    <Muze data={dataModel}>
      <Canvas
        width={'1440px'}
        height={'900px'}
        columns={['ProductCategories.name', 'Orders.createdAt.month']}
        rows={['Orders.count', 'Users.city']}
        color={'Users.gender'}
      >
        <Layer mark="bar"></Layer>
      </Canvas>
    </Muze>
  );
}

And with just that, we get a beautifully faceted, cross-interactive visualization!

J. Conclusion

Whew! That was a long one. And we learned a lot! To recap we:

  • installed PostgreSQL
  • set it up with an authenticated user with only the necessary database permissions
  • loaded some data into it
  • created a Cube.js back-end project
  • generated the Cube.js schema for our database
  • created a React based front-end project
  • connected the back-end and the front-end
  • brought data from the database to the front end by querying data using Cube.js
  • created a DataModel
  • rendered a simple chart using MuzeJS and React
  • sliced and diced the chart it till we could visualize our exploratory question

That's all folks! There's a ton more features that MuzeJS has and the possibilities open up even more when combined with Cube.js. You can check MuzeJS here and its many hand crafted demos showing its many features here. As mentioned at the start of the article we'll keep this article updated with links to posts about more cool stuff possible with MuzeJS and Cube.js. Keep an eye out!

We hope you found this article helpful! If you have any feedback or queries feel free to send an email to eng@charts.com. We'll get back to you quick!

Cube.js Digest

Subscribe for the Cube.js news, releases, and latest posts.

You may also like: