How To Build Custom Data Visualizations Using Luzmo Flex

Bringing data to life in your application can be done without the usual headaches. Paul Scanlon shows you how you can build beautiful data visualizations using the Google Analytics API, and you won’t have to spend any time “massaging” the data.

In this article, I’ll introduce you to Luzmo Flex, a new feature from the Luzmo team who have been working hard making developer tooling to flatten the on-ramp for analytics reporting and data visualization.

With Luzmo Flex, you can hook up a dataset and create beautifully crafted, fully customizable interactive charts that meet your reporting needs. They easily integrate and interact with other components of your web app, allowing you to move away from a traditional “dashboard” interface and build more bespoke data products.

While many charting libraries offer similar features, I often found it challenging to get the data into the right shape that the library needed. In this article, I’ll show you how you can build beautiful data visualizations using the Google Analytics API, and you won’t have to spend any time “massaging” the data!

What Is Luzmo Flex?

Well, it’s two things, really. First of all, Luzmo is a low-code platform for embedded analytics. You can create datasets from just about anything, connect them to APIs like Google Analytics or your PostgreSQL database, or even upload static data in a .csv file and start creating data visualizations with drag and drop.

Secondly, Luzmo Flex is their new React component that can be configured to create custom data visualizations. Everything from the way you query your data to the way you display it can be achieved through code using the LuzmoVizItemComponent.

What makes Luzmo Flex unique is that you can reuse the core functionalities of Luzmo’s low-code embedded analytics platform in your custom-coded components.

That means, besides creating ready-to-use datasets, you can set up functions like the following out-of-the-box:

  • Multi-tenant analytics: Showing different data or visualizations to different users of your app.
  • Localization: Displaying charts in multiple languages, currencies, and timezones without much custom development.
  • Interactivity: Set up event listeners to create complex interactivity between Luzmo’s viz items and any non-Luzmo components in your app.

What Can You Build With Luzmo Flex?

By combining these off-the-shelf functions with flexibility through code, Luzmo Flex makes a great solution for building bespoke data products that go beyond the limits of a traditional dashboard interface. Below are a few examples of what that could look like.

Report Builder

A custom report builder that lets users search and filter a dataset and render it out using a number of different charts.

A screenshot of a custom report builder
Luzmo Flex: Report builder. (Large preview)

Enable powerful filtering using HTML Select inputs, which will update each chart shown on the page.

Dashboard filter panel
Luzmo Flex:Dashboard filter panel. (Large preview)

Wearables Dashboard

Or how about a sleep tracker hooked up to your phone to track all those important snoozes?

Wearables Dashboard
Luzmo Flex: Wearables Dashboard. (Large preview)

When to Consider Luzmo Flex vs Chart Libraries

When building data-intensive applications, using something like Recharts, a well-known React charting library, you’ll likely need to reformat the data to fit the required shape. For instance, if I request the top 3 page views from the last seven days for my site, paulie.dev, I would have to use the Google Analytics API using the following query.

import dotenv from 'dotenv';
import { BetaAnalyticsDataClient } from '@google-analytics/data';
dotenv.config();

const credentials = JSON.parse(
  Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS_BASE64, 'base64').toString('utf-8')
);

const analyticsDataClient = new BetaAnalyticsDataClient({
  credentials,
});

const [{ rows }] = await analyticsDataClient.runReport({
  property: `properties/${process.env.GA4_PROPERTY_ID}`,
  dateRanges: [
    {
      startDate: '7daysAgo',
      endDate: 'today',
    },
  ],
  dimensions: [
    {
      name: 'fullPageUrl',
    },
    {
      name: 'pageTitle',
    },
  ],
  metrics: [
    {
      name: 'totalUsers',
    },
  ],
  limit: 3,
  metricAggregations: ['MAXIMUM'],
});

The response would look something like this:

[
  {
    "dimensionValues": [
      {
        "value": "www.paulie.dev/",
        "oneValue": "value"
      },
      {
        "value": "Paul Scanlon | Home",
        "oneValue": "value"
      }
    ],
    "metricValues": [
      {
        "value": "61",
        "oneValue": "value"
      }
    ]
  },
  {
    "dimensionValues": [
      {
        "value": "www.paulie.dev/posts/2023/11/a-set-of-sign-in-with-google-buttons-made-with-tailwind/",
        "oneValue": "value"
      },
      {
        "value": "Paul Scanlon | A set of: "Sign In With Google" Buttons Made With Tailwind",
        "oneValue": "value"
      }
    ],
    "metricValues": [
      {
        "value": "41",
        "oneValue": "value"
      }
    ]
  },
  {
    "dimensionValues": [
      {
        "value": "www.paulie.dev/posts/2023/10/what-is-a-proxy-redirect/",
        "oneValue": "value"
      },
      {
        "value": "Paul Scanlon | What Is a Proxy Redirect?",
        "oneValue": "value"
      }
    ],
    "metricValues": [
      {
        "value": "23",
        "oneValue": "value"
      }
    ]
  }
]

To make that data work with Recharts, I’d need to reformat it so it conforms to the following data shape.

[
  {
    "name": "Paul Scanlon | Home",
    "value": 61
  },
  {
    "name": "Paul Scanlon | A set of: "Sign In With Google" Buttons Made With Tailwind",
    "value": 41
  },
  {
    "name": "Paul Scanlon | What Is a Proxy Redirect?",
    "value": 23
  }
]

To accomplish this, I’d need to use an Array.prototype.map() to iterate over each item, destructure the relevant data and return a key-value pair for the name and value for each.

const data = response.rows.map((row) => {
  const { dimensionValues, metricValues } = row;

  const pageTitle = dimensionValues[1].value;
  const totalUsers = parseInt(metricValues[0].value);

  return {
    name: pageTitle,
    value: totalUsers,
  };
});

And naturally, if you’re reformatting data this way in your application, you’d also want to write unit tests to ensure the data is always formatted correctly to avoid breaking your application… and all of this before you even get on to creating your charts!

With Luzmo Flex, all of this goes away, leaving you more time to focus on which data to display and how best to display it.

The First Steps to Building Bespoke Data Products

Typically, when building user interfaces that display data insights, your first job will be to figure out how to query the data source. This can take many forms, from RESTful API requests to direct database queries or sometimes reading from static files. Your next job will be figuring out when and how often these requests need to occur.

  • For data that rarely changes: Perhaps a query in the build step will work.
  • For data that changes regularly: A server-side request on page load.
  • For ever-changing data: A client-side request that polls an API on an interval.

Each will likely inform your application’s architecture, and there’s no single solution to this. Your last job, as mentioned, will be wrangling the responses, reformatting the data, and displaying it in the UI.

Below, I’ll show you how to do this using Luzmo Flex by using a simple example product.

What We’re Building: Custom Data Visualizations As Code

Here’s a screenshot of a simple data product I’ve built that displays three different charts for different reporting dimensions exposed by the Google Analytics API for page views for my site, paulie.dev, from the last seven days.

You can find all the code used in this article on the following link:

A screenshot of a simple data product that displays three different charts for different reporting dimensions exposed by the Google Analytics API
(Large preview)

Getting Started With Luzmo

Before we get going, hop over to Luzmo and sign up for a free trial. You might also like to have a read of one of the getting started guides listed below. In this article, I’ll be using the Next.js starter.

Creating a Google Analytics Dataset

To create data visualization, you’ll first need data! To achieve this using Luzmo, head over to the dashboard, select Datasets from the navigation, and select GA4 Google Analytics. Follow the steps shown in the UI to connect Luzmo with your Google Analytics account.

A dashboard with the selected Datasets from the navigation and highlighted GA4 Google Analytics
(Large preview)

With the setup complete, you can now select which reporting dimensions to add to your dataset. To follow along with this article, select Custom selection.

A screenshot with the Custom selection selected
(Large preview)

Lastly, select the following using the search input. Device Category, Page Title, Date, and Total users, then click Import when you’re ready.

A screenshot with the selected Device Category, Page Title, Date, and Total users
(Large preview)

You now have all the data required to build the Google Analytics dashboard. You can access the dataset ID from the URL address bar in your browser. You’ll need this in a later step.

A screenshot with the highlighted URL address bar in a browser
(Large preview)

If you’ve followed along from either of the first two getting started guides, you’ll have your API Key, API Token, App server, and API host environment variables set up and saved in a .env file.

Install Dependencies

If you’ve cloned one of the starter repositories, run the following to install the required dependencies.

npm install

Next, install the Luzmo React Embed dependency which exports the LuzmoVizItemComponent.

npm install  @luzmo/react-embed@latest

Now, find page.tsx located in the src/app directory, and add your dataset id as shown below.

Add the access object from the destructured response and pass access.datasets[0].id onto the LuzmoClientComponent component using a prop named datasetId.

// src/app/page.tsx


+ import dynamic from 'next/dynamic';

import Luzmo from '@luzmo/nodejs-sdk';
- import LuzmoClientComponent from './components/luzmo-client-component';
+ const LuzmoClientComponent = dynamic(() => import('./components/luzmo-client-component'), {
  ssr: false,
});


const client = new Luzmo({
  api_key: process.env.LUZMO_API_KEY!,
  api_token: process.env.LUZMO_API_TOKEN!,
  host: process.env.NEXT_PUBLIC_LUZMO_API_HOST!,
});

export default async function Home() {
  const response = await client.create('authorization', {
    type: 'embed',
    username: 'user id',
    name: 'first name last name',
    email: 'name@email.com',
    access: {
      datasets: [
        {
-          id: '<dataset_id>',
+          id: '42b43db3-24b2-45e7-98c5-3fcdef20b1a3',
          rights: 'use',
        },
      ],
    },
  });

-  const { id, token } = response;
+  const { id, token, access } = response;

-  return <LuzmoClientComponent authKey={id} authToken={token} />;
+  return <LuzmoClientComponent authKey={id} authToken={token} datasetId={access.datasets[0].id} />;
}

And lastly, find luzmo-client-component.tsx located in src/app/components. This is where you’ll be creating your charts.

Building a Donut Chart

The first chart you’ll create is a Donut chart that shows the various devices used by visitors to your site.

A donut chart
(Large preview)

Add the following code to luzmo-client-component.tsx component.

// src/app/component/luzmo-client-component.tsx

'use client';

+ import { LuzmoVizItemComponent } from '@luzmo/react-embed';

interface Props {
  authKey: string;
  authToken: string;
+  datasetId: string;
}

- export default function LuzmoClientComponent({ authKey, authToken}: Props) {
+ export default function LuzmoClientComponent({ authKey, authToken, datasetId }: Props) {

+  const date = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(); // creates a date 7 days ago

  console.log({ authKey, authToken });

  return (
    <section>
+    <div className='w-1/2 h-80'>
+      <LuzmoVizItemComponent
+        appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
+        apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
+        authKey={authKey}
+        authToken={authToken}
+        type='donut-chart'
+        options={{
+          title: {
+            en: `Devices from last 7 days`,
+          },
+          display: {
+            title: true,
+          },
+          mode: 'donut',
+          legend: {
+            position: 'bottom',
+          },
+        }}
+        slots={[
+          {
+            name: 'measure',
+            content: [
+              {
+                label: {
+                  en: 'Total users',
+                },
+                column: '<column id>', // Total users
+                set: datasetId,
+                type: 'numeric',
+                format: '.4f',
+              },
+            ],
+          },
+          {
+            name: 'category',
+            content: [
+              {
+                label: {
+                  en: 'Device category',
+                },
+                column: '<column id>', // Device category
+                set: datasetId,
+                type: 'hierarchy',
+              },
+            ],
+          },
+        ]}
+        filters={[
+          {
+            condition: 'or',
+            filters: [
+              {
+                expression: '? >= ?',
+                parameters: [
+                  {
+                    column_id: '<column id>', // Date
+                    dataset_id: datasetId,
+                  },
+                  date,
+                ],
+              },
+            ],
+          },
+        ]}
+      />
+    <div/>
    </section>
  );
}

There’s quite a lot going on in the above code snippet, and I will explain it all in due course, but first, I’ll need to cover a particularly tricky part of the configuration.

Column IDs

You’ll notice the filters parameters, measure, and category content all require a column id.

In the filters parameters, the key is named column_id, and in the measure and category, the key is named column. Both of these are actually the column IDs from the dataset. And here’s how you can find them.

Back in the Luzmo dashboard, click into your dataset and look for the “more dots” next to each column heading. From the menu, select Copy column id. Add each column ID to the keys in the configuration objects.

In my example, I’m using the Total users for the measure, the Device category for the category, and the Date for the filter.

Luzmo dashboard
(Large preview)

If you’ve added the column IDs correctly, you should be able to see a rendered chart on your screen!

… and as promised, here’s a breakdown of the configuration.

Initial Props Donut chart

The first part is fairly straightforward. appServer and authKey are the environment variables you saved to your .env file, and authKey and authToken are destructured from the authorization request and passed into this component via props.

The type prop determines which type of chart to render. In my example, I’m using donut-chart, but you could choose from one of the many options available, area-chart, bar-chart, bubble-chart, box-plot, and many more. You can see all the available options in the Luzmo documentation under Chart docs.

<LuzmoVizItemComponent
  appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
  apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
  authKey={authKey}
  authToken={authToken}
  type='donut-chart'

The one thing I should point out is my use of Tailwind classes: w-1/2 (width: 50%) and h-80 (height: 20rem). The LuzmoVizItemComponent ships with height 100%, so you’ll need to wrap the component with an element that has an actual height, or you won’t be able to see the chart on the page as it could be 100% of the height of an element with no height.

Donut Chart Options

The options object is where you can customize the appearance of your chart. It accepts many configuration options, among which:

  • A title for the chart that accepts a locale with corresponding text to display.
  • A display title value to determine if the title is shown or not.
  • A mode to determine if the chart is to be of type donut or pie chart.
  • A legend option to determine where the legend can be positioned.

All the available configuration options can be seen in the Donut chart documentation.

options={{
  title: {
    en: `Devices from last 7 days`,
  },
  display: {
    title: true,
  },
  mode: 'donut',
  legend: {
    position: 'bottom',
  },
}}

Donut Chart Slots

Slots are where you can configure which column from your dataset to use for the category and measure.

Slots can contain multiple measures, useful for displaying two columns of data per chart, but if more than two are used, one will become the measure.

Each measure contains a content array. The content array, among many other configurations, can include the following:

  • A label and locale,
  • The column id from the dataset,
  • The datasetId,
  • The type of data you’re displaying,
  • A format for the data.

The format used here is Python syntax for floating-point numbers; it’s similar to JavaScript’s .toFixed() method, e.g number.toFixed(4).

The hierarchy type is ​​the Luzmo standard data type. Any text column is considered as an hierarchical data type.

You can read more in the Donut chart documentation about available configuration options for slots.

slots={[
  {
    name: 'measure',
    content: [
      {
        label: {
          en: 'Total users',
        },
        column: '<column id>', // Total users
        set: datasetId,
        type: 'numeric',
        format: '.4f',
      },
    ],
  },
  {
    name: 'category',
    content: [
      {
        label: {
          en: 'Device category',
        },
        column: '<column id>', // Device category
        set: datasetId,
        type: 'hierarchy',
      },
    ],
  },
]}

The filters object is where you can apply conditions that will determine which data will be shown. In my example, I only want to show data from the last seven days. To accomplish this, I first create the date variable:

const date = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();

This would produce an ISO date string, e.g., 2024-08-21T14:25:40.088Z, which I can use with the filter. The filter uses Luzmo’s Filter Expressions, to determine if the date for each row of the data is greater than or equal to the date variable. You can read more about Filter Expressions in Luzmo’s Academy article.

filters={[
  {
    condition: 'or',
    filters: [
      {
        expression: '? >= ?',
        parameters: [
          {
            column_id: '<column id>', // Date
            dataset_id: datasetId,
          },
          date,
        ],
      },
    ],
  },
]}

Building a Line Chart

The second chart you’ll be creating is a Line chart that displays the number of page views on each date from the last seven days from folks who visit your site.

A line chart
(Large preview)

Initial Props Line Chart

As with the Donut chart, the initial props are pretty much the same, but the type has been changed to line-chart.

<LuzmoVizItemComponent
  appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
  apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
  authKey={authKey}
  authToken={authToken}
  type='line-chart'

Line Chart Options

The options for the Line chart are as follows, and the mode has been changed to line-chart.

options={{
  title: {
    en: `Site visits from last 7 days`,
  },
  display: {
    title: true,
  },
  mode: 'grouped',
}}

Line Chart Slots

The slots object is almost the same as before with the Donut chart, but for the Line chart, I’m using the date column from the dataset instead of the device category, and instead of category, I’m using the x-axis slot type. To ensure I’m formatting the data correctly (by day), I’ve used level 5. You can read more about levels in the docs.

slots={[
  {
    name: 'measure',
    content: [
      {
        label: {
          en: 'Total users',
        },
        column: '<column id>', // Total users
        set: datasetId,
        type: 'numeric',
        format: '.4f',
      },
    ],
  },
  {
    name: 'x-axis',
    content: [
      {
        label: {
          en: 'Date',
        },
        column: '<column id>', // Date
        set: datasetId,
        type: 'datetime',
        level: 5,
      },
    ],
  },
]}

I’ve used the same filters as I used in the Donut chart.

Building a Bar Chart

The last chart you’ll be creating is a Bar chart that displays the number of page views for the top ten most viewed pages on your site.

A bar chart
(Large preview)

Initial Props Bar Chart

As with the Donut and Line chart, the initial props are pretty much the same, but the type has been changed to bar-chart.

<LuzmoVizItemComponent
  className='w-full h-80'
  appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
  apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
  authKey={authKey}
  authToken={authToken}
  type='bar-chart'

Bar Chart Options

The options for the Bar chart are a little more involved. I’ve included some styling options for the border-radii of the bars, limited the number of results to 10, and sorted the data by the highest page view count first using the sort by measure and direction options.

options={{
  title: {
    en: `Page views from last 7 days`,
  },
  display: {
    title: true,
  },
  mode: 'grouped',
  bars: {
    roundedCorners: 5,
  },
  limit: {
    number: 10,
  },
  sort: {
    by: 'measure',
    direction: 'desc',
  },
}}

Line Chart Slots

As with the Line chart, I’ve used an axis for one of the columns from the dataset. In this case, it’s the y-axis which displays the page title.

slots={[
  {
    name: 'measure',
    content: [
      {
        label: {
          en: 'Total users',
        },
        column: '<column id>', // Total users
        set: datasetId,
        type: 'numeric',
        format: '.4f',
      },
    ],
  },
  {
    name: 'y-axis',
    content: [
      {
        label: {
          en: 'Page title',
        },
        column: '<column id>', // Page title
        set: datasetId,
        type: 'hierarchy',
      },
    ],
  },
]}

I’ve used the same filters as I used in the Donut and Line chart.

What’s Next

As you can see, there are plenty of types of charts and customization options. Because this is just an “ordinary” React component, you can very easily make it configurable by an end user by allowing options to be set and unset using HTML input elements, checkbox, select, date, and so on.

But for me, the real power behind this is not having to mutate data!

This is particularly pertinent when displaying multiple charts with different reporting dimensions. Typically, this would require each to have their own utility function or reformatting method. That said, setting column IDs and dataset IDs is a little fiddly, but once you have the component hooked up to the dataset, you can configure and reconfigure as much as you like, all without having to rewrite data formatting functions.

If you’re interested in bringing data to life in your application and want to get it done without the usual headaches, book a free demo with the Luzmo team to learn more!

Smashing Editorial
(yk, il)

Similar Posts