-
Notifications
You must be signed in to change notification settings - Fork 47
Suggestions: Add plugin developer docs #2541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
fcb721a
Suggestions: Add plugin developer docs
fastfrwrd 909f7f8
updates from self-review
fastfrwrd 54bc8f7
slight simplification
fastfrwrd 027719d
remove note about alpha
fastfrwrd b4abffc
regenerate this one file
fastfrwrd e7c17a7
fix: add redirect for removed add-suggestion-supplier doc
fastfrwrd 087f6fa
docs: fix Suggestions casing and FieldType import typo
fastfrwrd 3a90bb4
docs: add visualization suggestions screenshot to how-it-works section
fastfrwrd 8bc49f1
Update docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-su…
fastfrwrd f725346
Update docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-su…
fastfrwrd 46c9497
Update docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-su…
fastfrwrd 94bccce
Update docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-su…
fastfrwrd 08368e5
Apply suggestion from @fastfrwrd
fastfrwrd b8ad10d
Apply suggestions from code review
fastfrwrd 1541dbf
Merge branch 'main' into fastfrwrd/suggestions-api-docs
fastfrwrd dedcb0f
Update docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-su…
fastfrwrd 5e00430
maxSeries and maxRows
adela-almasan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
341 changes: 341 additions & 0 deletions
341
docusaurus/docs/how-to-guides/panel-plugins/add-suggestions-support.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
fastfrwrd marked this conversation as resolved.
|
||
|
|
||
| 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. | ||
|
fastfrwrd marked this conversation as resolved.
|
||
|
|
||
|  | ||
|
|
||
| ### 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<MyPanelOptions>(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<MyPanelOptions>(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'; | ||
|
fastfrwrd marked this conversation as resolved.
|
||
|
|
||
| export const mySuggestionsSupplier: VisualizationSuggestionsSupplier<MyPanelOptions, MyFieldConfig> = (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<MyPanelOptions>(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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.