# Build a panel plugin

## Introduction[​](#introduction "Direct link to Introduction")

Panels, which allow you to visualize data in different ways, are one of the fundamental building blocks of Grafana. Grafana has several types of panels already included, and many more available in the [Grafana plugin catalog](https://grafana.com/grafana/plugins/).

To add support for other visualizations, you can create your own panel plugin. Panels are [ReactJS components](https://reactjs.org/docs/components-and-props.html) and can be scaffolded with the `create-plugin` tool.

For more information about panels, refer to the documentation on [Panels](https://grafana.com/docs/grafana/latest/panels/).

### Prerequisites[​](#prerequisites "Direct link to Prerequisites")

* Grafana v10.0 or later
* [LTS](https://nodejs.dev/en/about/releases/) version of Node.js

## Create a new plugin[​](#create-a-new-plugin "Direct link to Create a new plugin")

The Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin) is a CLI application that simplifies Grafana plugin development, so that you can focus on code. The tool scaffolds a starter plugin, all the required configuration, and a development environment using [Docker Compose](https://docs.docker.com/compose/) for you.

1. In a new directory, create a plugin from a template using the create-plugin tool. When prompted for the kind of plugin, select <!-- -->panel<!-- -->:

   ```shell
   npx @grafana/create-plugin@latest

   ```

2. Go to the directory of your newly created plugin:

   ```shell
   cd <your-plugin>

   ```

3. Install the dependencies:

   ```shell
   npm install

   ```

4. Build the plugin:

   ```shell
   npm run dev

   ```

5. Start Grafana:

   ```shell
   docker compose up

   ```

6. Open Grafana, by default <http://localhost:3000>, and then go to **Administration** > **Plugins**. Make sure that your <!-- -->panel<!-- --> plugin is there.

You can also verify that Grafana has discovered your plugin by checking the logs:

```text
INFO[01-01|12:00:00] Plugin registered       logger=plugin.loader pluginID=<your-plugin>

```

## Anatomy of a plugin[​](#anatomy-of-a-plugin "Direct link to Anatomy of a plugin")

Every plugin you create requires at least two files: `plugin.json` and `src/module.ts`.

### `plugin.json`[​](#pluginjson "Direct link to pluginjson")

When Grafana starts, it scans the [plugin directory](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#plugins) for any subdirectory that contains a `plugin.json` file. The `plugin.json` file contains information about your plugin and tells Grafana about what capabilities and dependencies your plugin needs.

While certain plugin types can have specific configuration options, let's look at the mandatory ones:

* `type` tells Grafana what type of plugin to expect. Grafana supports three types of plugins: `panel`, `datasource`, and `app`.
* `name` is what users will see in the list of plugins. If you're creating a data source, this is typically the name of the database it connects to, such as Prometheus, PostgreSQL, or Stackdriver.
* `id` uniquely identifies your plugin and should follow this naming convention: `<$organization-name>-<$plugin-name>-<$plugin-type>`. The create-plugin tool correctly configures this based on your responses to its prompts.

To see all the available configuration settings for the `plugin.json`, refer to the [plugin.json Schema](https://grafana.com/developers/plugin-tools/reference/plugin-json.md).

### `module.ts`[​](#modulets "Direct link to modulets")

After discovering your plugin, Grafana loads the `module.js` file, the entrypoint for your plugin. `module.js` exposes the implementation of your plugin, which depends on the type of plugin you're building.

Specifically, `src/module.ts` needs to export a class that extends [GrafanaPlugin](https://github.com/grafana/grafana/blob/f900098cc9f5771c02b6189ba5138547b4f5e6c2/packages/grafana-data/src/types/plugin.ts#L175), and can be any of the following:

* [PanelPlugin](https://github.com/grafana/grafana/blob/f900098cc9f5771c02b6189ba5138547b4f5e6c2/packages/grafana-data/src/panel/PanelPlugin.ts#L95)
* [DataSourcePlugin](https://github.com/grafana/grafana/blob/f900098cc9f5771c02b6189ba5138547b4f5e6c2/packages/grafana-data/src/types/datasource.ts#L33)
* [AppPlugin](https://github.com/grafana/grafana/blob/f900098cc9f5771c02b6189ba5138547b4f5e6c2/packages/grafana-data/src/types/app.ts#L58)

## Panel plugins[​](#panel-plugins "Direct link to Panel plugins")

### Panel properties[​](#panel-properties "Direct link to Panel properties")

The [PanelProps](https://github.com/grafana/grafana/blob/57960148e47e4d82e899dbfa3cb9b2d474ad56dc/packages/grafana-data/src/types/panel.ts#L74-L122) interface exposes runtime information about the panel, such as panel dimensions, and the current time range.

You can access the panel properties through the `props` argument, as seen in your plugin.

src/components/SimplePanel.tsx

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

```

## Development workflow[​](#development-workflow "Direct link to Development workflow")

Next, you'll learn the basic workflow of making a change to your panel, building it, and reloading Grafana to reflect the changes you made.

First, you need to add your panel to a dashboard:

### Add your panel to a dashboard[​](#add-your-panel-to-a-dashboard "Direct link to Add your panel to a dashboard")

1. Open Grafana in your browser. By default, Grafana is accessible at <http://localhost:3000>.

2. Create a new dashboard.

   * From the menu, select **Dashboards**.
   * On the top right, select **New** -> **Dashboard**.
   * Select **Add Visualization** to start configuring your new panel.

3. Configure the `TestData` data source. In the data source configuration modal, select the **TestData DB** data source for this dashboard.

4. Search and select your panel plugin. In the panel configuration view, go to the **Visualization** list on the right side, search for your panel plugin, and select it.

5. Save the dashboard.

***

### Alternative: Use the provisioned sample panel dashboard[​](#alternative-use-the-provisioned-sample-panel-dashboard "Direct link to Alternative: Use the provisioned sample panel dashboard")

You can also view your panel in action through a pre-configured dashboard:

1. Go to **Dashboards** and select the **Provisioned Sample Panel Dashboard**.
   <!-- -->
   * The **TestData DB** data source is already configured with sample data for this dashboard.
2. To edit, find your panel in this dashboard and click the menu (that is, the three vertical dots) at the top right corner of your panel.
3. From the dropdown menu, select **Edit** to customize or examine your panel's settings.

Now that you can view your panel, try making a change to the panel plugin:

1. In `SimplePanel.tsx`, change the fill color of the circle. For example, to change it to green:

   src/components/SimplePanel.tsx

   ```ts
   <circle style={{ fill: theme.visualization.getColorByName('green') }} r={100} />

   ```

2. Save the file.

3. In the browser, reload Grafana. The changes should appear.

## Add panel options[​](#add-panel-options "Direct link to Add panel options")

Sometimes you want to offer the users of your panel an option to configure the behavior of your plugin. By configuring *panel options* for your plugin, your panel will be able to accept user input.

In the previous step, you changed the fill color of the circle in the code. Let's change the code so that the plugin user can configure the color from the panel editor.

#### Add an option[​](#add-an-option "Direct link to Add an option")

Panel options are defined in a *panel options object*. `SimpleOptions` is an interface that describes the options object.

1. In `types.ts`, add a `CircleColor` type to hold the colors the users can choose from:

   ```text
   type CircleColor = 'red' | 'green' | 'blue';

   ```

2. In the `SimpleOptions` interface, add a new option called `color`:

   ```text
   color: CircleColor;

   ```

Here's the updated options definition:

src/types.ts

```ts
type SeriesSize = 'sm' | 'md' | 'lg';
type CircleColor = 'red' | 'green' | 'blue';

// interface defining panel options type
export interface SimpleOptions {
  text: string;
  showSeriesCount: boolean;
  seriesCountSize: SeriesSize;
  color: CircleColor;
}

```

#### Add an option control[​](#add-an-option-control "Direct link to Add an option control")

To change the option from the panel editor, you need to bind the `color` option to an *option control*.

Grafana supports a range of option controls, such as text inputs, switches, and radio groups.

Let's create a radio control and bind it to the `color` option.

1. Add the control at the end of the builder:

   src/module.ts

   ```ts
   .addRadio({
     path: 'color',
     name: 'Circle color',
     defaultValue: 'red',
     settings: {
       options: [
         {
           value: 'red',
           label: 'Red',
         },
         {
           value: 'green',
           label: 'Green',
         },
         {
           value: 'blue',
           label: 'Blue',
         },
       ],
     }
   });

   ```

   The `path` is used to bind the control to an option. You can bind a control to nested option by specifying the full path within a options object, for example `colors.background`.

Grafana builds an options editor for you and displays it in the panel editor sidebar in the **Display** section.

#### Use the new option[​](#use-the-new-option "Direct link to Use the new option")

You're almost done. You've added a new option and a corresponding control to change the value. But the plugin isn't using the option yet. Let's change that.

1. To convert option value to the colors used by the current theme, add the following statement right before the `return` statement in `SimplePanel.tsx`.

   src/components/SimplePanel.tsx

   ```ts
   let color = theme.visualization.getColorByName(options.color);

   ```

2. Configure the circle to use the color.

   src/components/SimplePanel.tsx

   ```tsx
   <g>
     <circle style={{ fill: color }} r={100} />
   </g>

   ```

Now, when you change the color in the panel editor, the fill color of the circle changes as well.

## Create dynamic panels using data frames[​](#create-dynamic-panels-using-data-frames "Direct link to Create dynamic panels using data frames")

Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series.

info

To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData](https://grafana.com/docs/grafana/latest/features/datasources/testdata) data source while developing.

When adding your panel to a dashboard, configure it with a data source to populate it with data dynamically. Alternatively, you can use the **Provisioned Sample Panel Dashboard**, which is already configured with a sample data source.

### Using the provisioned sample panel dashboard[​](#using-the-provisioned-sample-panel-dashboard "Direct link to Using the provisioned sample panel dashboard")

The Provisioned Sample Panel Dashboard comes preconfigured with the `TestData` data source. This setup includes sample data for testing and development. The sample data is in the `raw_frame` scenario of the [`TestData`](https://grafana.com/docs/grafana/latest/features/datasources/testdata) data source and consists of two time series with the same timestamps, as shown in the following table. The table displays these two time series joined by a timestamp:

| Timestamp           | Label1 | Value1 | Label2 | Value2 |
| ------------------- | ------ | ------ | ------ | ------ |
| 2020-12-31 19:00:00 | A      | 10     | A      | 40     |
| 2020-12-31 20:00:00 | B      | 20     | B      | 50     |
| 2020-12-31 21:00:00 | C      | 15     | C      | 45     |
| 2020-12-31 22:00:00 | D      | 25     | D      | 55     |
| 2020-12-31 23:00:00 | E      | 30     | E      | 60     |

This setup allows you to test your panel’s dynamic elements using real sample data, structured with multiple series for more complex visualization.

The results from a data source query within your panel are available in the `data` property inside your panel component.

```ts
const { data } = props;

```

The `data.series` contains the series returned from a data source query. Each series is represented as a data structure called *data frame*. A data frame resembles a table, where data is stored by columns, or *fields*, instead of rows. Every value in a field share the same data type, such as string, number, or time.

Here's an example of a data frame with a time field, `Time`, and a number field, `Value`:

| Time          | Value |
| ------------- | ----- |
| 1589189388597 | 32.4  |
| 1589189406480 | 27.2  |
| 1589189513721 | 15.0  |

Provisioned sample panel dashboard\*\* sample data:

| Timestamp           | Label1 | Value1 | Label2 | Value2 |
| ------------------- | ------ | ------ | ------ | ------ |
| 2020-12-31 19:00:00 | A      | 10     | A      | 40     |
| 2020-12-31 20:00:00 | B      | 20     | B      | 50     |
| 2020-12-31 21:00:00 | C      | 15     | C      | 45     |
| 2020-12-31 22:00:00 | D      | 25     | D      | 55     |
| 2020-12-31 23:00:00 | E      | 30     | E      | 60     |

Let's see how you can retrieve data from a data frame and use it in your visualization.

1. Get the last value of each field of type `number`, by adding the following to `SimplePanel.tsx`, before the `return` statement:

   src/components/SimplePanel.tsx

   ```ts
   const radii = data.series
     .map((series) => series.fields.find((field) => field.type === 'number'))
     .map((field) => field?.values[field.values.length - 1]);

   ```

   `radii` will contain the last values in each of the series that are returned from a data source query. You'll use these to set the radius for each circle.

2. Change the `svg` element to the following:

   src/components/SimplePanel.tsx

   ```tsx
   <svg
     className={styles.svg}
     width={width}
     height={height}
     xmlns="http://www.w3.org/2000/svg"
     xmlnsXlink="http://www.w3.org/1999/xlink"
     viewBox={`0 -${height / 2} ${width} ${height}`}
   >
     <g fill={color}>
       {radii.map((radius, index) => {
         const step = width / radii.length;
         return <circle r={radius} transform={`translate(${index * step + step / 2}, 0)`} />;
       })}
     </g>
   </svg>

   ```

   Note how we're creating a `<circle>` element for each value in `radii`:

   src/components/SimplePanel.tsx

   ```tsx
   {
     radii.map((radius, index) => {
       const step = width / radii.length;
       return <circle r={radius} transform={`translate(${index * step + step / 2}, 0)`} />;
     });
   }

   ```

   We use the `transform` here to distribute the circle horizontally within the panel.

3. Rebuild your plugin and try it out by adding multiple queries to the panel. Refresh the dashboard.

If you want to know more about data frames, check out our introduction to [Data frames](https://grafana.com/developers/plugin-tools/key-concepts/data-frames.md).

## Summary[​](#summary "Direct link to Summary")

In this tutorial you learned how to create a custom visualization for your dashboards.

## Next steps[​](#next-steps "Direct link to Next steps")

* [Add visualization suggestions to panel plugins](https://grafana.com/developers/plugin-tools/how-to-guides/panel-plugins/add-suggestions-support.md) — make your panel appear in Grafana's visualization picker by implementing a suggestions supplier.
* [Add data links support to panel plugins](https://grafana.com/developers/plugin-tools/how-to-guides/panel-plugins/add-datalinks-support.md) — allow users to navigate from your visualization to other dashboards or external URLs.
