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