Build a panel plugin with D3.js

Introduction

Panels are the building blocks of Grafana, and allow you to visualize data in different ways. This tutorial gives you a hands-on walkthrough of creating your own panel using D3.js.

For more information about panels, refer to the documentation on Panels.

In this tutorial, you’ll:

  • Build a simple panel plugin to visualize a bar chart.
  • Learn how to use D3.js to build a panel using data-driven transformations.

Prerequisites

  • Grafana 7.0
  • NodeJS 12.x
  • yarn

Set up your environment

Before you can get started building plugins, you need to set up your environment for plugin development.

To discover plugins, Grafana scans a plugin directory, the location of which depends on your operating system.

  1. Create a directory called grafana-plugins in your preferred workspace.

  2. Find the plugins property in the Grafana configuration file and set the plugins property to the path of your grafana-plugins directory. Refer to the Grafana configuration documentation for more information.

    [paths]
    plugins = "/path/to/grafana-plugins"
    
  3. Restart Grafana if it’s already running, to load the new configuration.

Alternative method: Docker

If you don’t want to install Grafana on your local machine, you can use Docker.

To set up Grafana for plugin development using Docker, run the following command:

docker run -d -p 3000:3000 -v "$(pwd)"/grafana-plugins:/var/lib/grafana/plugins --name=grafana grafana/grafana:7.0.0

Since Grafana only loads plugins on start-up, you need to restart the container whenever you add or remove a plugin.

docker restart grafana

Create a new plugin

Tooling for modern web development can be tricky to wrap your head around. While you certainly can write your own webpack configuration, for this guide, you’ll be using grafana-toolkit.

grafana-toolkit is a CLI application that simplifies Grafana plugin development, so that you can focus on code. The toolkit takes care of building and testing for you.

  1. In the plugin directory, create a plugin from template using the plugin:create command:

    npx @grafana/toolkit plugin:create my-plugin
    
  2. Change directory.

    cd my-plugin
    
  3. Download necessary dependencies:

    yarn install
    
  4. Build the plugin:

    yarn dev
    
  5. Restart the Grafana server for Grafana to discover your plugin.

  6. Open Grafana and go to Configuration -> Plugins. Make sure that your plugin is there.

By default, Grafana logs whenever it discovers a plugin:

INFO[01-01|12:00:00] Registering plugin       logger=plugins name=my-plugin

Data-driven documents

D3.js is a JavaScript library for manipulating documents based on data. It lets you transform arbitrary data into HTML, and is commonly used for creating visualizations.

Wait a minute. Manipulating documents based on data? That’s sounds an awful lot like React. In fact, much of what you can accomplish with D3 you can already do with React. So before we start looking at D3, let’s see how you can create an SVG from data, using only React.

In SimplePanel.tsx, change SimplePanel to return an svg with a rect element.

export const SimplePanel: React.FC<Props> = ({ options, data, width, height }) => {
  const theme = useTheme();

  return (
    <svg width={width} height={height}>
      <rect x={0} y={0} width={10} height={10} fill={theme.palette.greenBase} />
    </svg>
  );
};

One single rectangle might not be very exciting, so let’s see how you can create rectangles from data.

  1. Create some data that we can visualize.

    const values = [4, 8, 15, 16, 23, 42];
    
  2. Calculate the height of each bar based on the height of the panel.

    const barHeight = height / values.length;
    
  3. Inside a SVG group, g, create a rect element for every value in the dataset. Each rectangle uses the value as its width.

    return (
      <svg width={width} height={height}>
        <g>
          {values.map((value, i) => (
            <rect x={0} y={i * barHeight} width={value} height={barHeight - 1} fill={theme.palette.greenBase} />
          ))}
        </g>
      </svg>
    );
    
  4. Rebuild the plugin and reload your browser to see the changes you’ve made.

As you can see, React is perfectly capable of dynamically creating HTML elements. In fact, creating elements using React is often faster than creating them using D3.

So why would you use even use D3? In the next step, we’ll see how you can take advantage of D3’s data transformations.

Transform data using D3.js

In this step, you’ll see how you can transform data using D3 before rendering it using React.

D3 is already bundled with Grafana, and you can access it by importing the d3 package. However, we’re going to need the type definitions while developing.

  1. Install the D3 type definitions:

    yarn add --dev @types/d3
    
  2. Import d3 in SimplePanel.tsx.

    import * as d3 from 'd3';
    

In the previous step, we had to define the width of each bar in pixels. Instead, let’s use scales from the D3 library to make the width of each bar depend on the width of the panel.

Scales are functions that map a range of values to another range of values. In this case, we want to map the values in our datasets to a position within our panel.

  1. Create a scale to map a value between 0 and the maximum value in the dataset, to a value between 0 and the width of the panel. We’ll be using this to calculate the width of the bar.

    const scale = d3
      .scaleLinear()
      .domain([0, d3.max(values) || 0.0])
      .range([0, width]);
    
    
  2. Pass the value to the scale function to calculate the width of the bar in pixels.

    return (
      <svg width={width} height={height}>
        <g>
          {values.map((value, i) => (
            <rect x={0} y={i * barHeight} width={scale(value)} height={barHeight - 1} fill={theme.palette.greenBase} />
          ))}
        </g>
      </svg>
    );
    

As you can see, even if we’re using React to render the actual elements, the D3 library contains useful tools that you can use to transform your data before rendering it.

Add an axis

Another useful tool in the D3 toolbox is the ability to generate axes. Adding axes to our chart makes it easier for the user to understand the differences between each bar.

Let’s see how you can use D3 to add a horizontal axis to your bar chart.

  1. Create a D3 axis. Notice that by using the same scale as before, we make sure that the bar width aligns with the ticks on the axis.

    const axis = d3.axisBottom(scale);
    
  2. Generate the axis. While D3 needs to generate the elements for the axis, we can encapsulate it by generating them within an anonymous function which we pass as a ref to a group element g.

    <g
      ref={node => {
        d3.select(node).call(axis as any);
      }}
    />
    

By default, the axis renders at the top of the SVG element. We’d like to move it to the bottom, but to do that, we first need to make room for it by decreasing the height of each bar.

  1. Calculate the new bar height based on the padded height.

    const padding = 20;
    const chartHeight = height - padding;
    const barHeight = chartHeight / values.length;
    
  2. Translate the axis by adding a transform to the g element.

    <g
      transform={`translate(0, ${chartHeight})`}
      ref={node => {
        d3.select(node).call(axis as any);
      }}
    />
    

Congrats! You’ve created a simple and responsive bar chart.

Complete example

import React from 'react';
import { PanelProps } from '@grafana/data';
import { SimpleOptions } from 'types';
import { useTheme } from '@grafana/ui';
import * as d3 from 'd3';

interface Props extends PanelProps<SimpleOptions> {}

export const SimplePanel: React.FC<Props> = ({ options, data, width, height }) => {
  const theme = useTheme();

  const values = [4, 8, 15, 16, 23, 42];

  const scale = d3
    .scaleLinear()
    .domain([0, d3.max(values) || 0.0])
    .range([0, width]);

  const axis = d3.axisBottom(scale);

  const padding = 20;
  const chartHeight = height - padding;
  const barHeight = chartHeight / values.length;

  return (
    <svg width={width} height={height}>
      <g>
        {values.map((value, i) => (
          <rect x={0} y={i * barHeight} width={scale(value)} height={barHeight - 1} fill={theme.palette.greenBase} />
        ))}
      </g>
      <g
        transform={`translate(0, ${chartHeight})`}
        ref={node => {
          d3.select(node).call(axis as any);
        }}
      />
    </svg>
  );
};

Congratulations

Congratulations, you made it to the end of this tutorial!