mirror of https://github.com/grafana/grafana
TrendPanel: Add new trend panel (Alpha) (#65740)
parent
313b3dd2af
commit
d974e5f25a
@ -0,0 +1,137 @@ |
|||||||
|
import React, { useMemo } from 'react'; |
||||||
|
|
||||||
|
import { FieldType, PanelProps } from '@grafana/data'; |
||||||
|
import { config, PanelDataErrorView } from '@grafana/runtime'; |
||||||
|
import { KeyboardPlugin, TimeSeries, TooltipDisplayMode, TooltipPlugin, usePanelContext } from '@grafana/ui'; |
||||||
|
import { findFieldIndex } from 'app/features/dimensions'; |
||||||
|
|
||||||
|
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin'; |
||||||
|
import { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils'; |
||||||
|
|
||||||
|
import { PanelOptions } from './panelcfg.gen'; |
||||||
|
|
||||||
|
export const TrendPanel = ({ |
||||||
|
data, |
||||||
|
timeRange, |
||||||
|
timeZone, |
||||||
|
width, |
||||||
|
height, |
||||||
|
options, |
||||||
|
fieldConfig, |
||||||
|
replaceVariables, |
||||||
|
id, |
||||||
|
}: PanelProps<PanelOptions>) => { |
||||||
|
const { sync } = usePanelContext(); |
||||||
|
|
||||||
|
const info = useMemo(() => { |
||||||
|
if (data.series.length > 1) { |
||||||
|
return { |
||||||
|
warning: 'Only one frame is supported, consider adding a join transformation', |
||||||
|
frames: data.series, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
let frames = data.series; |
||||||
|
let xFieldIdx: number | undefined; |
||||||
|
if (options.xField) { |
||||||
|
xFieldIdx = findFieldIndex(frames[0], options.xField); |
||||||
|
if (xFieldIdx == null) { |
||||||
|
return { |
||||||
|
warning: 'Unable to find field: ' + options.xField, |
||||||
|
frames: data.series, |
||||||
|
}; |
||||||
|
} |
||||||
|
} else { |
||||||
|
// first number field
|
||||||
|
// Perhaps we can/should support any ordinal rather than an error here
|
||||||
|
xFieldIdx = frames[0].fields.findIndex((f) => f.type === FieldType.number); |
||||||
|
if (xFieldIdx === -1) { |
||||||
|
return { |
||||||
|
warning: 'No numeric fields found for X axis', |
||||||
|
frames, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Make sure values are ascending
|
||||||
|
if (xFieldIdx != null) { |
||||||
|
const field = frames[0].fields[xFieldIdx]; |
||||||
|
if (field.type === FieldType.number) { |
||||||
|
// we may support ordinal soon
|
||||||
|
let last = Number.NEGATIVE_INFINITY; |
||||||
|
const values = field.values.toArray(); |
||||||
|
for (let i = 0; i < values.length; i++) { |
||||||
|
const v = values[i]; |
||||||
|
if (last > v) { |
||||||
|
return { |
||||||
|
warning: `Values must be in ascending order (index: ${i}, ${last} > ${v})`, |
||||||
|
frames, |
||||||
|
}; |
||||||
|
} |
||||||
|
last = v; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) }; |
||||||
|
}, [data, options.xField]); |
||||||
|
|
||||||
|
if (info.warning || !info.frames) { |
||||||
|
return ( |
||||||
|
<PanelDataErrorView |
||||||
|
panelId={id} |
||||||
|
fieldConfig={fieldConfig} |
||||||
|
data={data} |
||||||
|
message={info.warning} |
||||||
|
needsNumberField={true} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<TimeSeries // Name change! |
||||||
|
frames={info.frames} |
||||||
|
structureRev={data.structureRev} |
||||||
|
timeRange={timeRange} |
||||||
|
timeZone={timeZone} |
||||||
|
width={width} |
||||||
|
height={height} |
||||||
|
legend={options.legend} |
||||||
|
options={options} |
||||||
|
> |
||||||
|
{(config, alignedDataFrame) => { |
||||||
|
if ( |
||||||
|
alignedDataFrame.fields.filter((f) => f.config.links !== undefined && f.config.links.length > 0).length > 0 |
||||||
|
) { |
||||||
|
alignedDataFrame = regenerateLinksSupplier(alignedDataFrame, info.frames!, replaceVariables, timeZone); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<KeyboardPlugin config={config} /> |
||||||
|
{options.tooltip.mode === TooltipDisplayMode.None || ( |
||||||
|
<TooltipPlugin |
||||||
|
frames={info.frames!} |
||||||
|
data={alignedDataFrame} |
||||||
|
config={config} |
||||||
|
mode={options.tooltip.mode} |
||||||
|
sortOrder={options.tooltip.sort} |
||||||
|
sync={sync} |
||||||
|
timeZone={timeZone} |
||||||
|
/> |
||||||
|
)} |
||||||
|
|
||||||
|
<ContextMenuPlugin |
||||||
|
data={alignedDataFrame} |
||||||
|
frames={info.frames!} |
||||||
|
config={config} |
||||||
|
timeZone={timeZone} |
||||||
|
replaceVariables={replaceVariables} |
||||||
|
defaultItems={[]} |
||||||
|
/> |
||||||
|
</> |
||||||
|
); |
||||||
|
}} |
||||||
|
</TimeSeries> |
||||||
|
); |
||||||
|
}; |
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,30 @@ |
|||||||
|
import { PanelPlugin } from '@grafana/data'; |
||||||
|
import { commonOptionsBuilder } from '@grafana/ui'; |
||||||
|
|
||||||
|
import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config'; |
||||||
|
|
||||||
|
import { TrendPanel } from './TrendPanel'; |
||||||
|
import { PanelFieldConfig, PanelOptions } from './panelcfg.gen'; |
||||||
|
import { TrendSuggestionsSupplier } from './suggestions'; |
||||||
|
|
||||||
|
export const plugin = new PanelPlugin<PanelOptions, PanelFieldConfig>(TrendPanel) |
||||||
|
.useFieldConfig(getGraphFieldConfig(defaultGraphConfig)) |
||||||
|
.setPanelOptions((builder) => { |
||||||
|
const category = ['X Axis']; |
||||||
|
builder.addFieldNamePicker({ |
||||||
|
path: 'xField', |
||||||
|
name: 'X Field', |
||||||
|
description: 'An increasing numeric value', |
||||||
|
category, |
||||||
|
defaultValue: undefined, |
||||||
|
settings: { |
||||||
|
isClearable: true, |
||||||
|
placeholderText: 'First numeric value', |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
commonOptionsBuilder.addTooltipOptions(builder); |
||||||
|
commonOptionsBuilder.addLegendOptions(builder); |
||||||
|
}) |
||||||
|
.setSuggestionsSupplier(new TrendSuggestionsSupplier()); |
||||||
|
//.setDataSupport({ annotations: true, alertStates: true });
|
||||||
@ -0,0 +1,42 @@ |
|||||||
|
// Copyright 2021 Grafana Labs |
||||||
|
// |
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
// you may not use this file except in compliance with the License. |
||||||
|
// You may obtain a copy of the License at |
||||||
|
// |
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
// |
||||||
|
// Unless required by applicable law or agreed to in writing, software |
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
// See the License for the specific language governing permissions and |
||||||
|
// limitations under the License. |
||||||
|
|
||||||
|
package grafanaplugin |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/grafana/grafana/packages/grafana-schema/src/common" |
||||||
|
) |
||||||
|
|
||||||
|
composableKinds: PanelCfg: { |
||||||
|
lineage: { |
||||||
|
seqs: [ |
||||||
|
{ |
||||||
|
schemas: [ |
||||||
|
{ |
||||||
|
// Identical to timeseries... except it does not have timezone settings |
||||||
|
PanelOptions: { |
||||||
|
legend: common.VizLegendOptions |
||||||
|
tooltip: common.VizTooltipOptions |
||||||
|
|
||||||
|
// Name of the x field to use (defaults to first number) |
||||||
|
xField?: string |
||||||
|
} @cuetsy(kind="interface") |
||||||
|
|
||||||
|
PanelFieldConfig: common.GraphFieldConfig & {} @cuetsy(kind="interface") |
||||||
|
}, |
||||||
|
] |
||||||
|
}, |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// Generated by:
|
||||||
|
// public/app/plugins/gen.go
|
||||||
|
// Using jennies:
|
||||||
|
// TSTypesJenny
|
||||||
|
// PluginTSTypesJenny
|
||||||
|
//
|
||||||
|
// Run 'make gen-cue' from repository root to regenerate.
|
||||||
|
|
||||||
|
import * as common from '@grafana/schema'; |
||||||
|
|
||||||
|
export const PanelCfgModelVersion = Object.freeze([0, 0]); |
||||||
|
|
||||||
|
/** |
||||||
|
* Identical to timeseries... except it does not have timezone settings |
||||||
|
*/ |
||||||
|
export interface PanelOptions { |
||||||
|
legend: common.VizLegendOptions; |
||||||
|
tooltip: common.VizTooltipOptions; |
||||||
|
/** |
||||||
|
* Name of the x field to use (defaults to first number) |
||||||
|
*/ |
||||||
|
xField?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface PanelFieldConfig extends common.GraphFieldConfig {} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
{ |
||||||
|
"type": "panel", |
||||||
|
"name": "Trend", |
||||||
|
"id": "trend", |
||||||
|
|
||||||
|
"state": "alpha", |
||||||
|
|
||||||
|
"info": { |
||||||
|
"description": "Like timeseries, but when x != time", |
||||||
|
"author": { |
||||||
|
"name": "Grafana Labs", |
||||||
|
"url": "https://grafana.com" |
||||||
|
}, |
||||||
|
"logos": { |
||||||
|
"small": "img/trend.svg", |
||||||
|
"large": "img/trend.svg" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
import { VisualizationSuggestionsBuilder } from '@grafana/data'; |
||||||
|
import { GraphDrawStyle, GraphFieldConfig } from '@grafana/schema'; |
||||||
|
import { SuggestionName } from 'app/types/suggestions'; |
||||||
|
|
||||||
|
import { PanelOptions } from './panelcfg.gen'; |
||||||
|
|
||||||
|
export class TrendSuggestionsSupplier { |
||||||
|
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { |
||||||
|
const { dataSummary } = builder; |
||||||
|
|
||||||
|
if (dataSummary.numberFieldCount < 2 || dataSummary.rowCountTotal < 2 || dataSummary.rowCountTotal < 2) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Super basic
|
||||||
|
const list = builder.getListAppender<PanelOptions, GraphFieldConfig>({ |
||||||
|
name: SuggestionName.LineChart, |
||||||
|
pluginId: 'trend', |
||||||
|
options: { |
||||||
|
legend: {} as any, |
||||||
|
}, |
||||||
|
fieldConfig: { |
||||||
|
defaults: { |
||||||
|
custom: {}, |
||||||
|
}, |
||||||
|
overrides: [], |
||||||
|
}, |
||||||
|
cardOptions: { |
||||||
|
previewModifier: (s) => { |
||||||
|
s.options!.legend.showLegend = false; |
||||||
|
|
||||||
|
if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { |
||||||
|
s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
return list; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue