Building a Next.js Dashboard with Dynamic Charts and SSR

Cover of the 'Building a Next.js Dashboard with Dynamic Charts and SSR' blog post

Data visualizations and analytics provide you with a graphical representation of your organization's data and can help you make data-driven decisions for your business. Building an analytics application for your organization's data is easier than you think.

In this tutorial, you will learn how to build a robust data analytics application with a dashboard using Next.js, Cube and Bizcharts. We'll build a Next.js dashboard like this one...

demo_app.gif

...and then upgrade it to use server-side rendering (SSR):

demo_app2.gif

Here’s a quick preview of what we are building. You can also find the complete finished code in this GitHub repository.

So, what is Cube?

Cube is an open-source, API-first headless business intelligence platform. Cube connects to dozens of different data sources (primarily databases) to make data accessible and consistent across every application.

You run your Cube API as a service. Cube manages connections to your databases and exposes an API for your front-end applications to consume and build data visualization and other analytics features.

https://cubedev-blog-images.s3.us-east-2.amazonaws.com/fd2bf0fb-3bd4-4528-99a4-46ddaab774c9.png

Getting started with Cube

The easiest way to get started with Cube is with Cube Cloud. It provides a fully managed cube server ready to use. However, if you prefer self-hosting, then follow this tutorial.

We will create a new Cube deployment in Cube Cloud. You can select a cloud platform of your choice.

create deployment of cube

Next, select start from scratch to get started with a fresh instance.

set_up_cube.png

Next, we will select a database. For this example application I am going to use PostgreSQL. Feel free to use any database of your choice.

Don’t have a database with sample data? We got you covered. We created a public database for you to connect your Cube instance and play around. Add the following credentials in your Cube database configuration to connect to our public database.

Hostname: demo-db-examples.cube.dev
Port: 5432
Database: ecom
Username: cube
Password: 12345

Cube can auto-generate a Data Schema from your SQL tables. A Cube.js Data Schema is used to model raw data into meaningful business definitions. The data schema is exposed through the querying API, allowing end-users to query a wide variety of analytical queries.

We will select the following tables for schema generation. More on Data Schema later in the article.

line_item
orders
products
product_categories
users
suppliers

Generate Schema It will take couple minutes to get up and running.

Visualizing data with the Developer Playground

Let’s head over to the Playground tab in Cube cloud. The Developer Playground is a tool to generate dashboards from various templates for different front-end frameworks and data visualization libraries.

We will create a new query. Let’s select Line Item Count as measures and Line Item Create at as time. We can select a framework and a data visualization library in the chart configuration. Then select Run to generate the data chart.

playground_overview

Once the chart is generated we can also select the edit button to view the code for the front end in code sandbox. This is extremely powerful because Cube scaffolds front-end code and gives us a template to build our front-end application.

bar chart example

Next, we will create a new Next.js application and create an analytics dashboard.

Building the Next.js App

Please run the following command to create a new Next.js app.

$ npx create-next-app myanalytics
$ cd myanalytics

Next, add the required npm packages to your project.

$ npm i @cubejs-client/react \
bizcharts \
antd \
react-flatpickr --save

Create a new .env.local file in the root directory of your project. Add the following environment variables.

# .env.local
NEXT_PUBLIC_CUBEJS_API_URL='<Your-Cube-API-Endpoint>'
NEXT_PUBLIC_CUBEJS_TOKEN='Your-Cube-Token'

You can find the Cube API endpoint from the Cube dashboard. Navigate to Settings from the Cube dashboard. There is a field called Cube.js API in the overview tab. Copy the url from there and add it to your .env.local.

cube_api.png

We will also need to generate a Cube token to connect to Cube Cloud from our Next.js application. Please Select the Env vars tab in your Settings and copy the CUBEJS_API_SECRET value.

settings.png

With this secret, we can generate a JWT token. You can run the following node script to generate a JWT token.

const jwt = require('jsonwebtoken');
const CUBE_API_SECRET = '<Secret>';
const cubejsToken = jwt.sign(
{}, CUBE_API_SECRET, { expiresIn: '30d' }
);
console.log(cubejsToken);

Learn more about JWT tokens and how they work on Auth0 website.

Copy the generated JWT token and add it to NEXT_PUBLIC_CUBEJS_TOKEN in .env.local file. We are now all set. Let’s go ahead and run our application with npm run dev command.

Creating our first chart

Let's create a chart to visualize our order counts for each day over a time period. Replace the contents of pages/index.js with the following code.

import { useEffect, useState } from 'react';
import cubejs from '@cubejs-client/core';
import Flatpickr from 'react-flatpickr';
import LineChart from '../components/LineChart'
import { stackedChartData } from '../util';
import Link from 'next/link';
import styles from '../styles/Home.module.css';
const cubejsApi = cubejs(
process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
{ apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
);
export default function Home() {
const [data, setData] = useState(null);
const [error, setError] = useState (null);
const [dateRange, setDateRange] = useState({
startDate: '2017-08-02',
endDate: '2018-01-31'
});
useEffect(() => {
loadData(); // function to load data from Cube
}, [dateRange]);
/**
* This function fetches data from Cube's api
**/
const loadData = () => {
cubejsApi
.load({
measures: ["Orders.count"],
timeDimensions: [
{
dimension: "Orders.createdAt",
granularity: `day`,
dateRange: [dateRange.startDate, dateRange.endDate]
}
]
})
.then((resultSet) => {
setData(stackedChartData(resultSet));
})
.catch((error) => {
setError(error);
});
}
if(error) {
return <div>Error: {error.message}</div>
}
if(!data) {
return <div>Loading...</div>
}
return (
<div className={styles.container}>
<h1>Client Rendered Charts Example</h1>
<h5>🗓️ Select a date range</h5>
<Flatpickr
options={{
allowInput: true,
mode: "range",
minDate: new Date('2016-12-12'),
maxDate: new Date('2020-12-12')
}}
value={[dateRange.startDate, dateRange.endDate]}
onChange={(selectedDates) => {
if (selectedDates.length === 2) {
setDateRange({
startDate: selectedDates[0],
endDate: selectedDates[1]
})
}
}}
/>
<h3>📈 Order count timeseries</h3>
<LineChart data={data}/>
</div>
)
}

Let's review the code. First of all, we are initializing the Cube.js API client with the following lines of code.

const cubejsApi = cubejs(
process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
{ apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
);

Inside the useEffect() hook we run a function called loadData. Inside the loadData function we call the load function from cubejsApi. This function queries Cube Cloud and returns the desired data based on the defined Cube schema.

// ...
useEffect(() => {
loadData();
}, [dateRange]);
const jsonQueryPlot = {
measures: ["Orders.count"],
timeDimensions: [
{
dimension: "Orders.createdAt",
granularity: `day`,
dateRange: [dateRange.startDate, dateRange.endDate]
}
]
}
const loadData = () => {
cubejsApi
.load(jsonQueryPlot)
.then((resultSet) => {
setData(stackedChartData(resultSet));
})
.catch((error) => {
setError(error);
});
}

Notice that we pass in an object as a parameter in the load function. The shape of this object defines the type of data we are getting back.

We can generate this object from the Cube Playground. Let's head over to Cube Playground and execute a query. Select the JSON Query tab as shown in the following image.

json_query.png

Notice, we also import a component named LineChart from the components/LineChart file. We will pass the data as props to this component to create the chart. Let's create a new file components/LineChart.js and add the following code.

import { Chart, Axis, Tooltip, Geom } from "bizcharts"
export default function LineChart({ data }) {
return (
<Chart
scale={{
x: {
tickCount: 8
}
}}
autoFit
height={400}
data={data}
forceFit
>
<Axis name="x" />
<Axis name="measure" />
<Tooltip
crosshairs={{
type: "y"
}}
/>
<Geom type="line" position="x*measure" size={2} color="color" />
</Chart>
)
}

Similarly, I will add a bar chart to visualize the order count by suppliers and a table for order count. The final version of pages/index.js should be as follows.

import { useEffect, useState } from 'react';
import cubejs from "@cubejs-client/core";
import Flatpickr from "react-flatpickr";
import LineChart from '../components/LineChart'
import { stackedChartData } from '../util';
import Link from 'next/link';
import styles from '../styles/Home.module.css';
**import BarChart from '../components/BarChart';
import TableRenderer from '../components/Table';**
const cubejsApi = cubejs(
process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
{ apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
);
export default function Home() {
const [data, setData] = useState(null);
const [barChartData, setBarChartData] = useState(null);
const [error, setError] = useState (null);
const [dateRange, setDateRange] = useState({
startDate: '2017-08-02',
endDate: '2018-01-31'
});
useEffect(() => {
loadData();
}, [dateRange]);
const jsonQueryPlot = {
measures: ["Orders.count"],
timeDimensions: [
{
dimension: "Orders.createdAt",
granularity: `day`,
dateRange: [dateRange.startDate, dateRange.endDate]
}
]
}
const jsonQueryBarChart = {
measures: ["Orders.count"],
timeDimensions: [
{
dimension: "Orders.createdAt",
dateRange: [dateRange.startDate, dateRange.endDate]
}
],
order: {
"Orders.count": "desc"
},
dimensions: ["Suppliers.company"],
"filters": []
}
const loadData = () => {
cubejsApi
.load(jsonQueryPlot)
.then((resultSet) => {
setData(stackedChartData(resultSet));
})
.catch((error) => {
setError(error);
})
**cubejsApi
.load(jsonQueryBarChart)
.then((resultSet) => {
setBarChartData(stackedChartData(resultSet));
})
.catch((error) => {
setError(error);
})**
}
if(error) {
return <div>Error: {error.message}</div>
}
if(!data || !barChartData) {
return <div>Loading...</div>
}
return (
<div className={styles.container}>
<Link href={`/ssr-example?startDate=2017-08-02&endDate=2018-01-31`}>
<a className={styles.link}>View SSR Example</a>
</Link>
<h1>Client Rendered Charts Example</h1>
<h5>🗓️ Select a date range</h5>
<Flatpickr
options={{
allowInput: true,
mode: "range",
minDate: new Date('2016-12-12'),
maxDate: new Date('2020-12-12')
}}
value={[dateRange.startDate, dateRange.endDate]}
onChange={(selectedDates) => {
if (selectedDates.length === 2) {
setDateRange({
startDate: selectedDates[0],
endDate: selectedDates[1]
})
}
}}
/>
<h3>📈 Order count timeseries</h3>
<LineChart data={data}/>
**<h3>📊 Order count by Suppliers</h3>
<BarChart
data={barChartData}
pivotConfig={{
x: ["Suppliers.company"],
y: ["measures"],
fillMissingDates: true,
joinDateRange: false
}}
/>
<h3>📋 Order Table</h3>
<TableRenderer data={barChartData} />**
</div>
)
}

Fetching dashboard data with SSR (Server Side Rendering)

Next.js provides the ability to make an API call on the server-side. You do this with getServerSideProps function. You can learn more about it in the Next.js docs.

We can add the Cube API calls inside the getServerSideProps function and fetch all the data necessary for our dashboard on the server-side. When the page loads, the client (browser) doesn’t need to make additional API requests.

Let’s create a new page to pages/ssr-example.js and add the following code.

import cubejs from '@cubejs-client/core'
import styles from '../styles/Home.module.css'
import { stackedChartData } from '../util';
import LineChart from '../components/LineChart';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import BarChart from '../components/BarChart';
import TableRenderer from '../components/Table';
import Flatpickr from "react-flatpickr";
import { useRouter } from 'next/router';
export default function SSRCube({ data, barChartData, error }) {
const [_, setLoading] = useState(true);
const router = useRouter();
const { startDate, endDate } = router.query;
useEffect(() => {
if (data) {
process.nextTick(() => {
setLoading(false);
});
}
} , [data]);
return (
<div className={styles.container}>
<Link href={`/`}>
<a className={styles.link}>Client Rendered Example</a>
</Link>
<h1>SSR Charts Example</h1>
<h5>🗓️ Select a date range</h5>
<Flatpickr
options={{
allowInput: true,
mode: "range",
minDate: new Date('2016-12-12'),
maxDate: new Date('2020-12-12')
}}
value={[startDate, endDate]}
onChange={(selectedDates) => {
if (selectedDates.length === 2) {
router.push(`/ssr-example?startDate=${selectedDates[0]}&endDate=${selectedDates[1]}`);
}
}}
/>
<h3>📈 Order count timeseries</h3>
<LineChart data={data} />
<h3>📊 Order count by Suppliers</h3>
<BarChart
data={barChartData}
pivotConfig={{
x: ["Suppliers.company"],
y: ["measures"],
fillMissingDates: true,
joinDateRange: false
}}
/>
<h3>📋 Order Table</h3>
<TableRenderer data={barChartData} />
</div>
)
}
export async function getServerSideProps({ query }) {
const cubejsApi = cubejs(
process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
{ apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
);
const { startDate, endDate } = query;
try {
const resultSet = await cubejsApi
.load({
measures: ["Orders.count"],
timeDimensions: [
{
dimension: "Orders.createdAt",
granularity: `day`,
dateRange: query ? [startDate, endDate] : ['2017-08-02', '2018-01-31']
}
]
});
const barChartResult = await cubejsApi
.load({
measures: ["Orders.count"],
timeDimensions: [
{
dimension: "Orders.createdAt",
dateRange: query ? [startDate, endDate] : ['2017-08-02', '2018-01-31']
}
],
order: {
"Orders.count": "desc"
},
dimensions: ["Suppliers.company"],
"filters": []
})
return {
props: {
data: stackedChartData(resultSet),
barChartData: stackedChartData(barChartResult)
}
}
} catch (error) {
return {
props: {
error
}
}
}
}

The key difference is that we put all the API calls inside the getServerSideProps function. Then we pass the data to our page component as props.

When is SSR useful?

Server-side rendered applications load faster on the client-side as they make all API calls on the server-side. This may not be noticeable in fast network but you can clearly notice the difference in a slow network.

Following is a screenshot of client side rendering in a 3G network. Notice it makes 2 API calls and roughly takes about 5 seconds to load the page.

slow-3g.png

Now, compare this to the SSR version. The SSR version will make one API call as it rendered all data in the server side. It makes one call and it takes about 2 seconds.

Next.js also caches data so the performance can be optimized more.

ssr.png

If you are expecting a lot of data for your dashboard and want to improve user experience regardless of the client’s network speed, then SSR is the way to go.

Where to go from here?

In this tutorial, we build a simple metrics dashboard using Cube and Next.js. Cube comes with tons of features for data analytics and visualization. One of the best places to learn about these features is Cube's official documentation page.

The complete source code for this tutorial is available in this GitHub repo.

Check out our tutorials and blog for more sample apps and blog posts like this one.

And join us on Slack! It is a great place to get help and stay up to date.

Share this article