Build a Panel Plugin with D3

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.

What you’ll build

In this tutorial, you’ll build a simple, but complete bar graph panel.

What you’ll learn

  • How to use D3.js to build a panel using data-driven transformations.

What you’ll need

  • Grafana version 7.0+
  • NodeJS
  • 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"

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

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 you 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 panel 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 transformations

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.

In fact, D3.js is already bundled with Grafana, and you can access it by importing the d3 package.

  • Import the select function from d3:

SimplePanel.tsx

import { select } from 'd3';
  • Create a function called draw, where we’ll construct our chart, and call it in componentDidMount, and componentDidUpdate. By doing this, the render function returns a prebuilt chart to avoid rebuilding the chart on every call to render.
class SimplePanel extends PureComponent<Props> {
  containerElement: any;

  componentDidMount() {
    this.draw();
  }

  componentDidUpdate() {
    this.draw();
  }

  draw() {
    const { width, height } = this.props;

    const chart = select(this.containerElement)
      .html('')
      .attr('width', width)
      .attr('height', height)
      .text('Hello, world!');
  }

  render() {
    return <div ref={element => (this.containerElement = element)}></div>;
  }
}

Notice that, in the render function, the ref attribute lets you replace the div with your containerElement.

  • Run yarn dev, and reload Grafana to reflect the changes you’ve made.

When you add the panel to your dashboard, it will have the text ‘Hello, world!’ written in it.

Build a chart from data

You’ve seen how to use D3.js to create a container element with some hard-coded text in it. Next, you’ll build the graph from actual data.

  • Update the draw function with the following code:
draw() {
  const { width, height } = this.props;

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

  const maxValue = Math.max.apply(Math, data.map(o => o));

  const chart = select(this.containerElement)
    .html('')
    .attr('width', width)
    .attr('height', height);

  chart
    .selectAll('div')
    .data(data)
    .enter()
    .append('div')
    .style('height', height / data.length + 'px')
    .style('width', d => (d * width) / maxValue + 'px')
    .style('background-color', 'blue');
}
  • Run yarn dev, and reload Grafana to see a bar chart that dynamically resizes to fit the panel.

Congratulations, you’ve created a dynamic bar chart! Still, you’ve only touched the surface of what’s possible with D3. To learn more, check out the D3 Gallery.

Theme your panel

To provide your users with a consistent look-and-feel, you’ll want to use the same colors as the built-in panels.

In this step, you’ll learn how to use the colors from the current theme.

  • In SimplePanel.tsx, add a GrafanaTheme property to the PanelProps.
interface Props extends PanelProps<SimpleOptions> {
  theme: GrafanaTheme;
}

GrafanaTheme is available from the grafana/data package:

import { PanelProps, GrafanaTheme } from '@grafana/data';

The theme property is not set by default, so you need to use the withTheme to provide the current theme to the panel.

  • Rename SimplePanel to PartialSimplePanel.
class PartialSimplePanel extends PureComponent<Props>
  • Import withTheme from grafana/ui.
import { withTheme } from '@grafana/ui';
  • Export the SimplePanel, now complete with a theme. withTheme assigns the current theme to the theme property.
export const SimplePanel = withTheme(PartialSimplePanel);

The theme property is now available from within the component.

const { width, height, theme } = this.props;
  • Replace the current background color with a color from the theme.
style('background-color', theme.colors.red)

Complete example

import React, { PureComponent } from 'react';
import { withTheme } from '@grafana/ui';
import { PanelProps, GrafanaTheme } from '@grafana/data';

import { SimpleOptions } from 'types';
import { select } from 'd3';

interface Props extends PanelProps<SimpleOptions> {
  theme: GrafanaTheme;
}

class PartialSimplePanel extends PureComponent<Props> {
  containerElement: any;

  componentDidMount() {
    this.draw();
  }

  componentDidUpdate() {
    this.draw();
  }

  draw() {
    const { width, height, theme } = this.props;

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

    const maxValue = Math.max.apply(
      Math,
      data.map(o => o)
    );

    const chart = select(this.containerElement)
      .html('')
      .attr('width', width)
      .attr('height', height);

    chart
      .selectAll('div')
      .data(data)
      .enter()
      .append('div')
      .style('height', height / data.length + 'px')
      .style('width', d => (d * width) / maxValue + 'px')
      .style('background-color', theme.colors.red);
  }

  render() {
    return <div ref={element => (this.containerElement = element)}></div>;
  }
}

export const SimplePanel = withTheme(PartialSimplePanel);

Congratulations

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