diff --git a/docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-support.md b/docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-support.md new file mode 100644 index 0000000000..fc5436e43b --- /dev/null +++ b/docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-support.md @@ -0,0 +1,341 @@ +--- +id: add-suggestions-support +title: Implement Visualization Suggestions for panel plugins +description: How to set up Visualization Suggestions for panel plugins so they appear in the Grafana visualization picker. +keywords: + - grafana + - plugins + - plugin + - suggestions + - visualization + - panel +--- + +# Add Visualization Suggestions to panel plugins + +Grafana's Visualization Suggestions feature shows users a ranked list of panel types that are suitable for their current data. In Grafana 13, Visualization Suggestions became the default way to select a visualization type in the visualization picker, and we opened up the ability for external plugins to provide their own Visualization Suggestions. This guide explains how to implement Visualization Suggestions for your external panel plugin so it appears in that list. + +## Prerequisites + +- Basic knowledge of Grafana panel plugin development +- Familiarity with TypeScript +- A panel plugin with a `module.ts` entry point + +## How Suggestions work + +When a user opens the visualization picker, Grafana calls each panel plugin's Suggestions supplier with a summary of the current panel data. Each plugin returns the `VisualizationSuggestion` objects (if any) describing how to configure the plugin for that data. Grafana then ranks all Suggestions and presents them to the user. + +![The Grafana visualization picker showing the Suggestions tab with ranked panel options](/img/viz-suggestions-grafana-13.png) + +### Ranking order + +Suggestions are sorted by the following priority: + +1. **Core plugins always rank above external plugins**, regardless of score. An external plugin with a score of `Best` will appear below a core plugin with a score of `OK`. +2. Within each tier (core or external), suggestions are sorted by score in descending order. +3. Within the same tier and score, suggestions that match the data's `preferredVisualisationType` rank higher. + +Return an accurate score so your plugin surfaces correctly relative to other external plugins when the data is a good fit. + +### The `suggestions` field in `plugin.json` + +To opt your plugin in to the Suggestions system, set `"suggestions": true` in your `plugin.json`: + +```json title="plugin.json" +{ + "type": "panel", + "name": "My Panel", + "id": "myorg-mypanel-panel", + "suggestions": true +} +``` + +Without this field, Grafana will not call your supplier. + +## Set up the supplier + +Call `setSuggestionsSupplier` on your `PanelPlugin` instance in `module.ts`. The supplier is a function that receives a [`PanelDataSummary`](#understand-paneldatasummary) and returns an array of `VisualizationSuggestion` objects, or `void` (no suggestions): + +```ts title="module.ts" +import { PanelPlugin } from '@grafana/data'; +import { MyPanel } from './MyPanel'; +import { MyPanelOptions } from './types'; + +export const plugin = new PanelPlugin(MyPanel).setSuggestionsSupplier((dataSummary) => { + // Return void (or nothing) when your plugin cannot meaningfully + // visualize this data. + if (!dataSummary.hasData) { + return; + } + + return [ + { + name: 'My panel', + }, + ]; +}); +``` + +## Understand `PanelDataSummary` + +The `PanelDataSummary` object gives you a pre-computed summary of the current data frames. Use the summary methods and attributes over `rawFrames` — they are faster and cover the most common cases: + +| API | Description | +| ----------------------------------------------------------- | -------------------------------------------------------------- | +| `hasData` | `true` when there is at least one row of data | +| `frameCount` | Number of data frames | +| `rowCountTotal` | Total rows across all frames | +| `rowCountMax` | Maximum rows in any single frame | +| `fieldCount` | Total fields across all frames | +| `fieldCountMax` | Maximum fields in any single frame | +| `isInstant` | `true` when all time values are the same (snapshot queries) | +| `hasFieldType(FieldType)` | `true` if any field matches the given type | +| `fieldCountByType(FieldType)` | Count of fields matching the given type across all frames | +| `hasDataFrameType(DataFrameType)` | `true` if any frame has the given `meta.type` | +| `hasPreferredVisualisationType(PreferredVisualisationType)` | `true` if any frame declares a preferred viz type | +| `rawFrames` | Direct access to the raw `DataFrame` array for deep inspection | + +Use `rawFrames` only when you need to inspect field names, custom metadata, or other details that the summary methods do not cover. The summary methods are computed once when the data changes and are much cheaper to call repeatedly. + +## Simple case: a single suggestion + +If your panel has a narrow data requirement (for example, it only works with a specific frame format), return a single suggestion when the data matches, and `void` otherwise. + +This is the pattern used by the Flame Graph panel. It inspects `rawFrames` to check for required fields because it needs to validate field-level metadata: + +```ts title="module.ts" +import { PanelPlugin, DataFrame, FieldType } from '@grafana/data'; + +import { MyPanel } from './MyPanel'; +import { MyPanelOptions } from './types'; + +function isValidData(frames: DataFrame[]): boolean { + // Check for a required field by name or type + return frames.some((frame) => + frame.fields.some((field) => field.name === 'level' && field.type === FieldType.number) + ); +} + +export const plugin = new PanelPlugin(MyPanel).setSuggestionsSupplier((dataSummary) => { + // Use rawFrames only when you need field-level inspection + if (!dataSummary.rawFrames || !isValidData(dataSummary.rawFrames)) { + return; + } + + return [ + { + name: 'My panel', + }, + ]; +}); +``` + +When you omit `name`, `options`, `score`, and `fieldConfig`, Grafana fills them in from the plugin's own defaults. A single `{}` entry is valid for the simplest possible suggestion. + +## Complex case: multiple suggestions with scoring + +If your panel supports multiple visualization variants (for example, line chart, bar chart, stacked area), return one suggestion per variant and assign a score based on how well the data fits. + +This is the pattern used by the Time series panel: + +```ts title="suggestions.ts" +import { + DataFrameType, + FieldType, + VisualizationSuggestionScore, + VisualizationSuggestionsSupplier, +} from '@grafana/data'; +import { MyPanelOptions, MyFieldConfig, GraphDrawStyle } from './types'; + +export const mySuggestionsSupplier: VisualizationSuggestionsSupplier = (dataSummary) => { + // Guard: this plugin requires time + number fields and more than one row + if ( + !dataSummary.hasFieldType(FieldType.time) || + !dataSummary.hasFieldType(FieldType.number) || + dataSummary.rowCountTotal < 2 + ) { + return; + } + + // Don't suggest this panel for instant (snapshot) queries + if (dataSummary.isInstant) { + return; + } + + // Score higher when the data explicitly declares itself as a time series type + const score: VisualizationSuggestionScore = + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesWide) || + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesLong) + ? VisualizationSuggestionScore.Good + : VisualizationSuggestionScore.OK; + + const suggestions = [ + { + name: 'Line chart', + fieldConfig: { + defaults: { custom: { drawStyle: GraphDrawStyle.Line, lineWidth: 1 } }, + overrides: [], + }, + }, + { + name: 'Bar chart', + options: { + custom: { + foo: true, + }, + }, + fieldConfig: { + defaults: { custom: { drawStyle: GraphDrawStyle.Bars } }, + overrides: [], + }, + }, + ]; + + // Apply score to all suggestions (score is only used if not already set on the suggestion) + return suggestions.map((s) => ({ score, ...s })); +}; +``` + +Then wire the supplier into `module.ts`: + +```ts title="module.ts" +import { PanelPlugin } from '@grafana/data'; +import { MyPanel } from './MyPanel'; +import { MyPanelOptions } from './types'; +import { mySuggestionsSupplier } from './suggestions'; + +export const plugin = new PanelPlugin(MyPanel).setSuggestionsSupplier(mySuggestionsSupplier); +``` + +Splitting the supplier into its own file keeps `module.ts` clean and makes the supplier independently testable. + +## Suggestion scores + +Use the `VisualizationSuggestionScore` enum to communicate how well your plugin fits the data: + +| Score constant | When to use | +| ----------------------------------- | ----------------------------------------------------------------------- | +| `VisualizationSuggestionScore.Best` | Your plugin is definitively the best option for this data | +| `VisualizationSuggestionScore.Good` | Your plugin is a strong match but not the only sensible choice | +| `VisualizationSuggestionScore.OK` | Your plugin can display this data, but other options may suit it better | + +If you do not set a score, Grafana defaults to `OK`. It is fine to omit the score unless you have a specific reason to rank a suggestion higher or lower. + +## Customize the suggestion card with `cardOptions` + +Suggestion preview cards render a live panel at a small size. The `cardOptions` object controls how the card looks and how much data it receives. Use it to keep preview cards fast and visually clear - especially important when users have large datasets. + +### Limit preview data with `maxSeries` and `maxRows` + +Preview cards render a real panel instance, so passing a full dataset into a small thumbnail can cause slow rendering. Use `maxSeries` and `maxRows` to cap the data before it reaches the card renderer: + +| Property | Type | Description | +| ----------- | -------- | ---------------------------------------------------------------- | +| `maxSeries` | `number` | Maximum number of data frames (series) passed to the preview | +| `maxRows` | `number` | Maximum number of rows per data frame passed to the preview | + +Pick limits based on what makes visual sense at preview scale. For example, a stat panel only needs a handful of series to convey its layout, while a bar chart benefits from capping rows to avoid rendering hundreds of thin bars: + +```ts +// Stat panel: limit to 6 series for a clean preview +cardOptions: { + maxSeries: 6, + previewModifier: (s) => { + if (s.options?.reduceOptions?.values) { + s.options.reduceOptions.limit = 6; + } + }, +}, + +// Bar chart: cap rows when the dataset is large +cardOptions: { + maxRows: 20, + previewModifier: (s) => { + s.options!.legend = { showLegend: false }; + }, +}, +``` + +### Adjust preview appearance with `previewModifier` + +The `previewModifier` function lets you adjust how a suggestion looks in the preview card. It is called just before the card is rendered, and you should mutate the suggestion object directly: + +```ts +cardOptions: { + previewModifier: (s) => { + // Common adjustments for preview cards: + // - Hide the legend (takes space, not useful at small scale) + // - Increase line widths (thin lines disappear at preview scale) + // - Disable keyboard/hover events + // - Force a simpler view mode + s.options = s.options ?? {}; + s.options.legend = { showLegend: false }; + }, +}, +``` + +The `previewModifier` only affects how the card is rendered - it does not change what gets applied when the user selects the suggestion. + +## Test your supplier + +Because the supplier is a plain function, you can test it directly to confirm its behavior: + +```ts title="suggestions.test.ts" +import { + createDataFrame, + DataFrameType, + FieldType, + getPanelDataSummary, + VisualizationSuggestionScore, +} from '@grafana/data'; +import { mySuggestionsSupplier } from './suggestions'; + +describe('mySuggestionsSupplier', () => { + it('returns void when there is no time field', () => { + const result = mySuggestionsSupplier( + getPanelDataSummary([ + createDataFrame({ + fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }], + }), + ]) + ); + expect(result).toBeUndefined(); + }); + + it('returns suggestions for time + number data', () => { + const result = mySuggestionsSupplier( + getPanelDataSummary([ + createDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [0, 100, 200] }, + { name: 'value', type: FieldType.number, values: [1, 2, 3] }, + ], + }), + ]) + ); + expect(result).toHaveLength(2); + expect(result![0].name).toBe('Line chart'); + }); + + it('scores Good for explicit time series frame types', () => { + const result = mySuggestionsSupplier( + getPanelDataSummary([ + createDataFrame({ + meta: { type: DataFrameType.TimeSeriesWide }, + fields: [ + { name: 'time', type: FieldType.time, values: [0, 100, 200] }, + { name: 'value', type: FieldType.number, values: [1, 2, 3] }, + ], + }), + ]) + ); + expect(result![0].score).toBe(VisualizationSuggestionScore.Good); + }); +}); +``` + +## Notes + +- Return `void` (or nothing) from your supplier when the data is not a good fit for your panel plugin. Never return an empty array — it signals that you have looked at the data and decided no variant is suitable, but the outcome is the same as `void` without allocating an extra array. +- The `name` field on a suggestion defaults to the plugin's display name from `plugin.json`. Override it only when you are returning multiple suggestions that need distinct names. +- `options` and `fieldConfig` in a suggestion are merged with the plugin's defaults, so you should only include the fields you want to override within a suggestion. diff --git a/docusaurus/docs/key-concepts/anatomy-of-a-plugin.md b/docusaurus/docs/key-concepts/anatomy-of-a-plugin.md index f263aeb13f..050c47a704 100644 --- a/docusaurus/docs/key-concepts/anatomy-of-a-plugin.md +++ b/docusaurus/docs/key-concepts/anatomy-of-a-plugin.md @@ -126,12 +126,20 @@ For more details on panel visualizations, refer to the [panel plugin tutorial](/ ### Panel options -Panel options allow users to customize the behavior and appearance of the panel plugin. You can define these options by implementing the `OptionsEditor` component, which can expose options relevant to the visualization. These options are passed into the panel’s `render()` function, allowing for dynamic updates based on user inputs. +Panel options allow users to customize the behavior and appearance of the panel plugin. You can define these options by implementing the `OptionsEditor` component, which can expose options relevant to the visualization. These options are passed into the panel's `render()` function, allowing for dynamic updates based on user inputs. You can see an example of how to implement panel options in the [basic panel tutorial](/tutorials/build-a-panel-plugin). ![An example of custom panel options on the right](./images/panel-options.png) +### Visualization suggestions + +Panel plugins can participate in Grafana's visualization picker by returning suggestions based on the current panel data. When a user opens the visualization picker in the panel editor, Grafana calls each plugin's suggestions supplier with a summary of the data and uses the results to rank and display suitable panel types. + +To opt in, set `"suggestions": true` in `plugin.json` and call `setSuggestionsSupplier` on your `PanelPlugin` instance in `module.ts`. The supplier receives a `PanelDataSummary` and returns an array of `VisualizationSuggestion` objects — or nothing if the plugin is not a good fit for the data. + +For a complete walkthrough, refer to [Add visualization suggestions to panel plugins](/how-to-guides/panel-plugins/add-suggestions-support). + ## Plugin folder structure Run the `create-plugin` tool to generate a new folder for your plugin. The plugin folder follows a standard naming convention (for example, `organization-pluginName-pluginType`) and contains all the necessary files for building, running, and testing your plugin. diff --git a/docusaurus/docs/migration-guides/angular-react/suggestion-supplier.md b/docusaurus/docs/migration-guides/angular-react/suggestion-supplier.md deleted file mode 100644 index 4697527cd6..0000000000 --- a/docusaurus/docs/migration-guides/angular-react/suggestion-supplier.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -id: add-suggestion-supplier -title: Add a suggestion supplier -sidebar_position: 9 -description: How to add a suggestion supplier to a plugin. -keywords: - - grafana - - plugins - - plugin - - React - - ReactJS - - Angular - - migration - - suggestion ---- - -# Angular to React: Add a suggestion supplier - -You can add a suggestion supplier to examine query data coming from a panel and suggest usage of the plugin for the type of data detected. This guide provides instructions for doing so along with links to relevant examples. - -A good example is the `stat` panel, which inspects the query resukts and ranks itself "high" for a single series, and "low" for multiple series (or even none). - -## Add the suggestion supplier - -Here is an example suggestion suppler seen as part of `module.ts`: - -```ts -import { MyDataSuggestionsSupplier } from './suggestions'; -... - -.setSuggestionsSupplier(new MyDataSuggestionsSupplier()); -``` - -Here is an example suggestion supplier derived from polystat: - -```ts -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { MyOptions } from './types'; - -export class MyDataSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary: ds } = builder; - - if (!ds.hasData) { - return; - } - if (!ds.hasNumberField) { - return; - } - - const list = builder.getListAppender({ - name: 'MyPanel', - pluginId: 'myorg-description-panel', - options: {}, - }); - - list.append({ - name: 'MyPanel', - }); - } -} -``` - -:::note - -When creating a suggestion supplier be certain the plugin can actually render something for the data being provided. - -If the suggestion supplier ranks the plugin high incorrectly, the end result will often display a blank panel and/or an error message. - -It's best to offer the plugin only to query data that matches well-known criteria that the plugin can process and visualize. - -::: - -## Additional resources - -Reference these suggestion suppliers to get ideas for further customization: - -- [Piechart panel](https://github.com/grafana/grafana/blob/main/public/app/plugins/panel/piechart/suggestions.ts#L7) - -The piechart panel checks the query for more than 30 rows return and does not offer itself as a visualization, even though it could display the data it will be nearly unreadable. - -- [Stat panel](https://github.com/grafana/grafana/blob/main/public/app/plugins/panel/stat/suggestions.ts#L7) - -Similar to the piechart panel, this plugin offers itself for data row results less than 10. It also sets default options based on the types of fields inside the query results. - -- [Heatmap panel](https://github.com/grafana/grafana/blob/main/public/app/plugins/panel/heatmap/suggestions.ts#L8) - -This panel does some processing on the data, and if there are any warnings generated, it omits itself from being offered. diff --git a/docusaurus/docs/reference/metadata.md b/docusaurus/docs/reference/metadata.md index aa1cfc36e5..16a5d51313 100644 --- a/docusaurus/docs/reference/metadata.md +++ b/docusaurus/docs/reference/metadata.md @@ -239,7 +239,7 @@ Resources to include in plugin. | Name | Type | Description | Required | | -------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | | **uid** | `string` | Unique identifier of the included resource
| | -| **type** | `string` | Possible values are: `"dashboard"`, `"page"`, `"panel"`, `"datasource"`
| | +| **type** | `string` | Possible values are: `"dashboard"`, `"page"`, `"panel"`, `"datasource"`
| ✅ | | **name** | `string` | | | | **component** | `string` | (Legacy) The Angular component to use for a page.
| | | **role** | `string` | The minimum role a user must have to see this page in the navigation menu.
Possible values are: `"Admin"`, `"Editor"`, `"Viewer"`
| | diff --git a/docusaurus/docs/tutorials/build-a-panel-plugin.md b/docusaurus/docs/tutorials/build-a-panel-plugin.md index cc3d2a6c07..426897cc62 100644 --- a/docusaurus/docs/tutorials/build-a-panel-plugin.md +++ b/docusaurus/docs/tutorials/build-a-panel-plugin.md @@ -289,3 +289,8 @@ If you want to know more about data frames, check out our introduction to [Data ## Summary In this tutorial you learned how to create a custom visualization for your dashboards. + +## Next steps + +- [Add visualization suggestions to panel plugins](/how-to-guides/panel-plugins/add-suggestions-support) — make your panel appear in Grafana's visualization picker by implementing a suggestions supplier. +- [Add data links support to panel plugins](/how-to-guides/panel-plugins/add-datalinks-support) — allow users to navigate from your visualization to other dashboards or external URLs. diff --git a/docusaurus/website/sidebars.ts b/docusaurus/website/sidebars.ts index 280d4c8599..90dcaa2aa6 100644 --- a/docusaurus/website/sidebars.ts +++ b/docusaurus/website/sidebars.ts @@ -152,6 +152,7 @@ const sidebars: SidebarsConfig = { 'how-to-guides/panel-plugins/migration-handler-for-panels', 'how-to-guides/panel-plugins/read-data-from-a-data-source', 'how-to-guides/panel-plugins/subscribe-events', + 'how-to-guides/panel-plugins/add-suggestions-support', ], }, @@ -324,7 +325,6 @@ const sidebars: SidebarsConfig = { 'migration-guides/angular-react/migrate-angularjs-configuration-settings-to-react', 'migration-guides/angular-react/angular-react-convert-from-time_series2', 'migration-guides/angular-react/targeting-older-releases', - 'migration-guides/angular-react/add-suggestion-supplier', ], }, 'migration-guides/migrate-from-toolkit', diff --git a/docusaurus/website/static/img/viz-suggestions-grafana-13.png b/docusaurus/website/static/img/viz-suggestions-grafana-13.png new file mode 100644 index 0000000000..4e01723a01 Binary files /dev/null and b/docusaurus/website/static/img/viz-suggestions-grafana-13.png differ diff --git a/docusaurus/website/websiteRedirects.json b/docusaurus/website/websiteRedirects.json index ef5ace156e..3590797011 100644 --- a/docusaurus/website/websiteRedirects.json +++ b/docusaurus/website/websiteRedirects.json @@ -146,13 +146,17 @@ { "from": ["/get-started/best-practices"], "to": "/key-concepts/best-practices" - }, + }, { "from": ["/key-concepts/ui-extensions"], "to": "/how-to-guides/ui-extensions/ui-extensions-concepts" - }, + }, { "from": ["/how-to-guides/ui-extensions/expose-a-lazy-loaded-component"], "to": "/how-to-guides/ui-extensions/expose-a-component" - } + }, + { + "from": ["/migration-guides/angular-react/add-suggestion-supplier"], + "to": "/how-to-guides/panel-plugins/add-suggestions-support" + } ]