mirror of
https://github.com/grafana/grafana.git
synced 2025-08-06 04:29:29 +08:00
GraphNG: add new alpha XY Chart (#30096)
This commit is contained in:
@ -6,7 +6,7 @@ import { LiveChannelSupport } from './live';
|
|||||||
export enum PluginState {
|
export enum PluginState {
|
||||||
alpha = 'alpha', // Only included if `enable_alpha` config option is true
|
alpha = 'alpha', // Only included if `enable_alpha` config option is true
|
||||||
beta = 'beta', // Will show a warning banner
|
beta = 'beta', // Will show a warning banner
|
||||||
stable = 'stable', // Will not show anything
|
stable = 'stable', // Will not show anything
|
||||||
deprecated = 'deprecated', // Will continue to work -- but not show up in the options to add
|
deprecated = 'deprecated', // Will continue to work -- but not show up in the options to add
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,16 +70,16 @@ export interface LineConfig {
|
|||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface AreaConfig {
|
export interface FillConfig {
|
||||||
fillColor?: string;
|
fillColor?: string;
|
||||||
fillOpacity?: number;
|
fillOpacity?: number;
|
||||||
fillGradient?: AreaGradientMode;
|
fillGradient?: FillGradientMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export enum AreaGradientMode {
|
export enum FillGradientMode {
|
||||||
None = 'none',
|
None = 'none',
|
||||||
Opacity = 'opacity',
|
Opacity = 'opacity',
|
||||||
Hue = 'hue',
|
Hue = 'hue',
|
||||||
@ -126,7 +126,7 @@ export interface HideSeriesConfig {
|
|||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig, AxisConfig {
|
export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig {
|
||||||
drawStyle?: DrawStyle;
|
drawStyle?: DrawStyle;
|
||||||
hideFrom?: HideSeriesConfig;
|
hideFrom?: HideSeriesConfig;
|
||||||
}
|
}
|
||||||
@ -162,8 +162,8 @@ export const graphFieldOptions = {
|
|||||||
] as Array<SelectableValue<AxisPlacement>>,
|
] as Array<SelectableValue<AxisPlacement>>,
|
||||||
|
|
||||||
fillGradient: [
|
fillGradient: [
|
||||||
{ label: 'None', value: AreaGradientMode.None },
|
{ label: 'None', value: FillGradientMode.None },
|
||||||
{ label: 'Opacity', value: AreaGradientMode.Opacity },
|
{ label: 'Opacity', value: FillGradientMode.Opacity },
|
||||||
{ label: 'Hue', value: AreaGradientMode.Hue },
|
{ label: 'Hue', value: FillGradientMode.Hue },
|
||||||
] as Array<SelectableValue<AreaGradientMode>>,
|
] as Array<SelectableValue<FillGradientMode>>,
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import { UPlotConfigBuilder } from './UPlotConfigBuilder';
|
import { UPlotConfigBuilder } from './UPlotConfigBuilder';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { expect } from '../../../../../../public/test/lib/common';
|
import { expect } from '../../../../../../public/test/lib/common';
|
||||||
import { AreaGradientMode, AxisPlacement, DrawStyle, PointVisibility, ScaleDistribution } from '../config';
|
import { FillGradientMode, AxisPlacement, DrawStyle, PointVisibility, ScaleDistribution } from '../config';
|
||||||
|
|
||||||
describe('UPlotConfigBuilder', () => {
|
describe('UPlotConfigBuilder', () => {
|
||||||
describe('default config', () => {
|
describe('default config', () => {
|
||||||
@ -352,7 +352,7 @@ describe('UPlotConfigBuilder', () => {
|
|||||||
scaleKey: 'scale-x',
|
scaleKey: 'scale-x',
|
||||||
lineColor: '#FFAABB',
|
lineColor: '#FFAABB',
|
||||||
fillOpacity: 50,
|
fillOpacity: 50,
|
||||||
fillGradient: AreaGradientMode.Opacity,
|
fillGradient: FillGradientMode.Opacity,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(builder.getConfig().series[1].fill).toBeInstanceOf(Function);
|
expect(builder.getConfig().series[1].fill).toBeInstanceOf(Function);
|
||||||
@ -364,7 +364,7 @@ describe('UPlotConfigBuilder', () => {
|
|||||||
drawStyle: DrawStyle.Line,
|
drawStyle: DrawStyle.Line,
|
||||||
scaleKey: 'scale-x',
|
scaleKey: 'scale-x',
|
||||||
fillOpacity: 50,
|
fillOpacity: 50,
|
||||||
fillGradient: AreaGradientMode.Opacity,
|
fillGradient: FillGradientMode.Opacity,
|
||||||
showPoints: PointVisibility.Auto,
|
showPoints: PointVisibility.Auto,
|
||||||
pointSize: 5,
|
pointSize: 5,
|
||||||
pointColor: '#00ff00',
|
pointColor: '#00ff00',
|
||||||
|
@ -4,15 +4,15 @@ import { getCanvasContext } from '../../../utils/measureText';
|
|||||||
import {
|
import {
|
||||||
DrawStyle,
|
DrawStyle,
|
||||||
LineConfig,
|
LineConfig,
|
||||||
AreaConfig,
|
FillConfig,
|
||||||
PointsConfig,
|
PointsConfig,
|
||||||
PointVisibility,
|
PointVisibility,
|
||||||
LineInterpolation,
|
LineInterpolation,
|
||||||
AreaGradientMode,
|
FillGradientMode,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import { PlotConfigBuilder } from '../types';
|
import { PlotConfigBuilder } from '../types';
|
||||||
|
|
||||||
export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig {
|
export interface SeriesProps extends LineConfig, FillConfig, PointsConfig {
|
||||||
drawStyle: DrawStyle;
|
drawStyle: DrawStyle;
|
||||||
scaleKey: string;
|
scaleKey: string;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
@ -89,10 +89,10 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
|||||||
return fillColor;
|
return fillColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = fillGradient ?? AreaGradientMode.None;
|
const mode = fillGradient ?? FillGradientMode.None;
|
||||||
let fillOpacityNumber = fillOpacity ?? 0;
|
let fillOpacityNumber = fillOpacity ?? 0;
|
||||||
|
|
||||||
if (mode !== AreaGradientMode.None) {
|
if (mode !== FillGradientMode.None) {
|
||||||
return getCanvasGradient({
|
return getCanvasGradient({
|
||||||
color: (fillColor ?? lineColor)!,
|
color: (fillColor ?? lineColor)!,
|
||||||
opacity: fillOpacityNumber / 100,
|
opacity: fillOpacityNumber / 100,
|
||||||
@ -160,7 +160,7 @@ function mapDrawStyleToPathBuilder(
|
|||||||
|
|
||||||
interface AreaGradientOptions {
|
interface AreaGradientOptions {
|
||||||
color: string;
|
color: string;
|
||||||
mode: AreaGradientMode;
|
mode: FillGradientMode;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +172,7 @@ function getCanvasGradient(opts: AreaGradientOptions): (self: uPlot, seriesIdx:
|
|||||||
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
|
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case AreaGradientMode.Hue:
|
case FillGradientMode.Hue:
|
||||||
const color1 = tinycolor(color)
|
const color1 = tinycolor(color)
|
||||||
.spin(-25)
|
.spin(-25)
|
||||||
.darken(30)
|
.darken(30)
|
||||||
@ -186,7 +186,7 @@ function getCanvasGradient(opts: AreaGradientOptions): (self: uPlot, seriesIdx:
|
|||||||
gradient.addColorStop(0, color2);
|
gradient.addColorStop(0, color2);
|
||||||
gradient.addColorStop(1, color1);
|
gradient.addColorStop(1, color1);
|
||||||
|
|
||||||
case AreaGradientMode.Opacity:
|
case FillGradientMode.Opacity:
|
||||||
default:
|
default:
|
||||||
gradient.addColorStop(
|
gradient.addColorStop(
|
||||||
0,
|
0,
|
||||||
|
@ -41,6 +41,7 @@ const tempoPlugin = async () =>
|
|||||||
import * as textPanel from 'app/plugins/panel/text/module';
|
import * as textPanel from 'app/plugins/panel/text/module';
|
||||||
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
|
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
|
||||||
import * as graphPanel from 'app/plugins/panel/graph/module';
|
import * as graphPanel from 'app/plugins/panel/graph/module';
|
||||||
|
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
|
||||||
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
||||||
import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
||||||
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
||||||
@ -83,6 +84,7 @@ const builtInPlugins: any = {
|
|||||||
'app/plugins/panel/text/module': textPanel,
|
'app/plugins/panel/text/module': textPanel,
|
||||||
'app/plugins/panel/timeseries/module': timeseriesPanel,
|
'app/plugins/panel/timeseries/module': timeseriesPanel,
|
||||||
'app/plugins/panel/graph/module': graphPanel,
|
'app/plugins/panel/graph/module': graphPanel,
|
||||||
|
'app/plugins/panel/xychart/module': xyChartPanel,
|
||||||
'app/plugins/panel/dashlist/module': dashListPanel,
|
'app/plugins/panel/dashlist/module': dashListPanel,
|
||||||
'app/plugins/panel/pluginlist/module': pluginsListPanel,
|
'app/plugins/panel/pluginlist/module': pluginsListPanel,
|
||||||
'app/plugins/panel/alertlist/module': alertListPanel,
|
'app/plugins/panel/alertlist/module': alertListPanel,
|
||||||
|
190
public/app/plugins/panel/timeseries/config.ts
Normal file
190
public/app/plugins/panel/timeseries/config.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { FieldColorModeId, FieldConfigProperty, FieldType, identityOverrideProcessor } from '@grafana/data';
|
||||||
|
import {
|
||||||
|
AxisPlacement,
|
||||||
|
DrawStyle,
|
||||||
|
GraphFieldConfig,
|
||||||
|
graphFieldOptions,
|
||||||
|
LineInterpolation,
|
||||||
|
LineStyle,
|
||||||
|
PointVisibility,
|
||||||
|
ScaleDistribution,
|
||||||
|
ScaleDistributionConfig,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
import { SeriesConfigEditor } from './HideSeriesConfigEditor';
|
||||||
|
import { ScaleDistributionEditor } from './ScaleDistributionEditor';
|
||||||
|
import { LineStyleEditor } from './LineStyleEditor';
|
||||||
|
import { SetFieldConfigOptionsArgs } from '@grafana/data/src/panel/PanelPlugin';
|
||||||
|
import { FillGradientMode } from '@grafana/ui/src/components/uPlot/config';
|
||||||
|
|
||||||
|
export const defaultGraphConfig: GraphFieldConfig = {
|
||||||
|
drawStyle: DrawStyle.Line,
|
||||||
|
lineInterpolation: LineInterpolation.Linear,
|
||||||
|
lineWidth: 1,
|
||||||
|
fillOpacity: 0,
|
||||||
|
fillGradient: FillGradientMode.None,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOptionsArgs<GraphFieldConfig> {
|
||||||
|
return {
|
||||||
|
standardOptions: {
|
||||||
|
[FieldConfigProperty.Color]: {
|
||||||
|
settings: {
|
||||||
|
byValueSupport: false,
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
mode: FieldColorModeId.PaletteClassic,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useCustomConfig: builder => {
|
||||||
|
builder
|
||||||
|
.addRadio({
|
||||||
|
path: 'drawStyle',
|
||||||
|
name: 'Style',
|
||||||
|
defaultValue: cfg.drawStyle,
|
||||||
|
settings: {
|
||||||
|
options: graphFieldOptions.drawStyle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'lineInterpolation',
|
||||||
|
name: 'Line interpolation',
|
||||||
|
defaultValue: cfg.lineInterpolation,
|
||||||
|
settings: {
|
||||||
|
options: graphFieldOptions.lineInterpolation,
|
||||||
|
},
|
||||||
|
showIf: c => c.drawStyle === DrawStyle.Line,
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'lineWidth',
|
||||||
|
name: 'Line width',
|
||||||
|
defaultValue: cfg.lineWidth,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
showIf: c => c.drawStyle !== DrawStyle.Points,
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'fillOpacity',
|
||||||
|
name: 'Fill opacity',
|
||||||
|
defaultValue: cfg.fillOpacity,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
showIf: c => c.drawStyle !== DrawStyle.Points,
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'fillGradient',
|
||||||
|
name: 'Fill gradient',
|
||||||
|
defaultValue: graphFieldOptions.fillGradient[0].value,
|
||||||
|
settings: {
|
||||||
|
options: graphFieldOptions.fillGradient,
|
||||||
|
},
|
||||||
|
showIf: c => !!(c.drawStyle !== DrawStyle.Points && c.fillOpacity && c.fillOpacity > 0),
|
||||||
|
})
|
||||||
|
.addCustomEditor<void, LineStyle>({
|
||||||
|
id: 'lineStyle',
|
||||||
|
path: 'lineStyle',
|
||||||
|
name: 'Line style',
|
||||||
|
showIf: c => c.drawStyle === DrawStyle.Line,
|
||||||
|
editor: LineStyleEditor,
|
||||||
|
override: LineStyleEditor,
|
||||||
|
process: identityOverrideProcessor,
|
||||||
|
shouldApply: f => f.type === FieldType.number,
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'spanNulls',
|
||||||
|
name: 'Null values',
|
||||||
|
defaultValue: false,
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ label: 'Gaps', value: false },
|
||||||
|
{ label: 'Connected', value: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
showIf: c => c.drawStyle === DrawStyle.Line,
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'showPoints',
|
||||||
|
name: 'Show points',
|
||||||
|
defaultValue: graphFieldOptions.showPoints[0].value,
|
||||||
|
settings: {
|
||||||
|
options: graphFieldOptions.showPoints,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'pointSize',
|
||||||
|
name: 'Point size',
|
||||||
|
defaultValue: 5,
|
||||||
|
settings: {
|
||||||
|
min: 1,
|
||||||
|
max: 40,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
showIf: c => c.showPoints !== PointVisibility.Never || c.drawStyle === DrawStyle.Points,
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'axisPlacement',
|
||||||
|
name: 'Placement',
|
||||||
|
category: ['Axis'],
|
||||||
|
defaultValue: graphFieldOptions.axisPlacement[0].value,
|
||||||
|
settings: {
|
||||||
|
options: graphFieldOptions.axisPlacement,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addTextInput({
|
||||||
|
path: 'axisLabel',
|
||||||
|
name: 'Label',
|
||||||
|
category: ['Axis'],
|
||||||
|
defaultValue: '',
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Optional text',
|
||||||
|
},
|
||||||
|
showIf: c => c.axisPlacement !== AxisPlacement.Hidden,
|
||||||
|
// no matter what the field type is
|
||||||
|
shouldApply: () => true,
|
||||||
|
})
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'axisWidth',
|
||||||
|
name: 'Width',
|
||||||
|
category: ['Axis'],
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Auto',
|
||||||
|
},
|
||||||
|
showIf: c => c.axisPlacement !== AxisPlacement.Hidden,
|
||||||
|
})
|
||||||
|
.addCustomEditor<void, ScaleDistributionConfig>({
|
||||||
|
id: 'scaleDistribution',
|
||||||
|
path: 'scaleDistribution',
|
||||||
|
name: 'Scale',
|
||||||
|
category: ['Axis'],
|
||||||
|
editor: ScaleDistributionEditor,
|
||||||
|
override: ScaleDistributionEditor,
|
||||||
|
defaultValue: { type: ScaleDistribution.Linear },
|
||||||
|
shouldApply: f => f.type === FieldType.number,
|
||||||
|
process: identityOverrideProcessor,
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'hideFrom',
|
||||||
|
name: 'Hide in area',
|
||||||
|
category: ['Series'],
|
||||||
|
path: 'hideFrom',
|
||||||
|
defaultValue: {
|
||||||
|
tooltip: false,
|
||||||
|
graph: false,
|
||||||
|
legend: false,
|
||||||
|
},
|
||||||
|
editor: SeriesConfigEditor,
|
||||||
|
override: SeriesConfigEditor,
|
||||||
|
shouldApply: () => true,
|
||||||
|
hideFromDefaults: true,
|
||||||
|
hideFromOverrides: true,
|
||||||
|
process: value => value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -12,7 +12,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
|
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
|
||||||
import {
|
import {
|
||||||
AreaGradientMode,
|
FillGradientMode,
|
||||||
AxisPlacement,
|
AxisPlacement,
|
||||||
DrawStyle,
|
DrawStyle,
|
||||||
LineInterpolation,
|
LineInterpolation,
|
||||||
@ -234,7 +234,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNumber(angular.fillGradient) && angular.fillGradient > 0) {
|
if (isNumber(angular.fillGradient) && angular.fillGradient > 0) {
|
||||||
graph.fillGradient = AreaGradientMode.Opacity;
|
graph.fillGradient = FillGradientMode.Opacity;
|
||||||
graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
|
graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,192 +1,13 @@
|
|||||||
import {
|
import { PanelPlugin } from '@grafana/data';
|
||||||
FieldColorModeId,
|
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
|
||||||
FieldConfigProperty,
|
|
||||||
FieldType,
|
|
||||||
identityOverrideProcessor,
|
|
||||||
PanelPlugin,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import {
|
|
||||||
AxisPlacement,
|
|
||||||
DrawStyle,
|
|
||||||
GraphFieldConfig,
|
|
||||||
graphFieldOptions,
|
|
||||||
LegendDisplayMode,
|
|
||||||
LineStyle,
|
|
||||||
PointVisibility,
|
|
||||||
ScaleDistribution,
|
|
||||||
ScaleDistributionConfig,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import { SeriesConfigEditor } from './HideSeriesConfigEditor';
|
|
||||||
import { TimeSeriesPanel } from './TimeSeriesPanel';
|
import { TimeSeriesPanel } from './TimeSeriesPanel';
|
||||||
import { graphPanelChangedHandler } from './migrations';
|
import { graphPanelChangedHandler } from './migrations';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { ScaleDistributionEditor } from './ScaleDistributionEditor';
|
import { getGraphFieldConfig, defaultGraphConfig } from './config';
|
||||||
import { LineStyleEditor } from './LineStyleEditor';
|
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(TimeSeriesPanel)
|
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(TimeSeriesPanel)
|
||||||
.setPanelChangeHandler(graphPanelChangedHandler)
|
.setPanelChangeHandler(graphPanelChangedHandler)
|
||||||
.useFieldConfig({
|
.useFieldConfig(getGraphFieldConfig(defaultGraphConfig))
|
||||||
standardOptions: {
|
|
||||||
[FieldConfigProperty.Color]: {
|
|
||||||
settings: {
|
|
||||||
byValueSupport: false,
|
|
||||||
},
|
|
||||||
defaultValue: {
|
|
||||||
mode: FieldColorModeId.PaletteClassic,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
useCustomConfig: builder => {
|
|
||||||
builder
|
|
||||||
.addRadio({
|
|
||||||
path: 'drawStyle',
|
|
||||||
name: 'Style',
|
|
||||||
defaultValue: graphFieldOptions.drawStyle[0].value,
|
|
||||||
settings: {
|
|
||||||
options: graphFieldOptions.drawStyle,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addRadio({
|
|
||||||
path: 'lineInterpolation',
|
|
||||||
name: 'Line interpolation',
|
|
||||||
defaultValue: graphFieldOptions.lineInterpolation[0].value,
|
|
||||||
settings: {
|
|
||||||
options: graphFieldOptions.lineInterpolation,
|
|
||||||
},
|
|
||||||
showIf: c => c.drawStyle === DrawStyle.Line,
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
|
||||||
path: 'lineWidth',
|
|
||||||
name: 'Line width',
|
|
||||||
defaultValue: 1,
|
|
||||||
settings: {
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
showIf: c => c.drawStyle !== DrawStyle.Points,
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
|
||||||
path: 'fillOpacity',
|
|
||||||
name: 'Fill opacity',
|
|
||||||
defaultValue: 0,
|
|
||||||
settings: {
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
showIf: c => c.drawStyle !== DrawStyle.Points,
|
|
||||||
})
|
|
||||||
.addRadio({
|
|
||||||
path: 'fillGradient',
|
|
||||||
name: 'Fill gradient',
|
|
||||||
defaultValue: graphFieldOptions.fillGradient[0].value,
|
|
||||||
settings: {
|
|
||||||
options: graphFieldOptions.fillGradient,
|
|
||||||
},
|
|
||||||
showIf: c => !!(c.drawStyle !== DrawStyle.Points && c.fillOpacity && c.fillOpacity > 0),
|
|
||||||
})
|
|
||||||
.addCustomEditor<void, LineStyle>({
|
|
||||||
id: 'lineStyle',
|
|
||||||
path: 'lineStyle',
|
|
||||||
name: 'Line style',
|
|
||||||
showIf: c => c.drawStyle === DrawStyle.Line,
|
|
||||||
editor: LineStyleEditor,
|
|
||||||
override: LineStyleEditor,
|
|
||||||
process: identityOverrideProcessor,
|
|
||||||
shouldApply: f => f.type === FieldType.number,
|
|
||||||
})
|
|
||||||
.addRadio({
|
|
||||||
path: 'spanNulls',
|
|
||||||
name: 'Null values',
|
|
||||||
defaultValue: false,
|
|
||||||
settings: {
|
|
||||||
options: [
|
|
||||||
{ label: 'Gaps', value: false },
|
|
||||||
{ label: 'Connected', value: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
showIf: c => c.drawStyle === DrawStyle.Line,
|
|
||||||
})
|
|
||||||
.addRadio({
|
|
||||||
path: 'showPoints',
|
|
||||||
name: 'Show points',
|
|
||||||
defaultValue: graphFieldOptions.showPoints[0].value,
|
|
||||||
settings: {
|
|
||||||
options: graphFieldOptions.showPoints,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
|
||||||
path: 'pointSize',
|
|
||||||
name: 'Point size',
|
|
||||||
defaultValue: 5,
|
|
||||||
settings: {
|
|
||||||
min: 1,
|
|
||||||
max: 40,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
showIf: c => c.showPoints !== PointVisibility.Never || c.drawStyle === DrawStyle.Points,
|
|
||||||
})
|
|
||||||
.addRadio({
|
|
||||||
path: 'axisPlacement',
|
|
||||||
name: 'Placement',
|
|
||||||
category: ['Axis'],
|
|
||||||
defaultValue: graphFieldOptions.axisPlacement[0].value,
|
|
||||||
settings: {
|
|
||||||
options: graphFieldOptions.axisPlacement,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addTextInput({
|
|
||||||
path: 'axisLabel',
|
|
||||||
name: 'Label',
|
|
||||||
category: ['Axis'],
|
|
||||||
defaultValue: '',
|
|
||||||
settings: {
|
|
||||||
placeholder: 'Optional text',
|
|
||||||
},
|
|
||||||
showIf: c => c.axisPlacement !== AxisPlacement.Hidden,
|
|
||||||
// no matter what the field type is
|
|
||||||
shouldApply: () => true,
|
|
||||||
})
|
|
||||||
.addNumberInput({
|
|
||||||
path: 'axisWidth',
|
|
||||||
name: 'Width',
|
|
||||||
category: ['Axis'],
|
|
||||||
settings: {
|
|
||||||
placeholder: 'Auto',
|
|
||||||
},
|
|
||||||
showIf: c => c.axisPlacement !== AxisPlacement.Hidden,
|
|
||||||
})
|
|
||||||
.addCustomEditor<void, ScaleDistributionConfig>({
|
|
||||||
id: 'scaleDistribution',
|
|
||||||
path: 'scaleDistribution',
|
|
||||||
name: 'Scale',
|
|
||||||
category: ['Axis'],
|
|
||||||
editor: ScaleDistributionEditor,
|
|
||||||
override: ScaleDistributionEditor,
|
|
||||||
defaultValue: { type: ScaleDistribution.Linear },
|
|
||||||
shouldApply: f => f.type === FieldType.number,
|
|
||||||
process: identityOverrideProcessor,
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'hideFrom',
|
|
||||||
name: 'Hide in area',
|
|
||||||
category: ['Series'],
|
|
||||||
path: 'hideFrom',
|
|
||||||
defaultValue: {
|
|
||||||
tooltip: false,
|
|
||||||
graph: false,
|
|
||||||
legend: false,
|
|
||||||
},
|
|
||||||
editor: SeriesConfigEditor,
|
|
||||||
override: SeriesConfigEditor,
|
|
||||||
shouldApply: () => true,
|
|
||||||
hideFromDefaults: true,
|
|
||||||
hideFromOverrides: true,
|
|
||||||
process: value => value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.setPanelOptions(builder => {
|
.setPanelOptions(builder => {
|
||||||
builder
|
builder
|
||||||
.addRadio({
|
.addRadio({
|
||||||
|
4
public/app/plugins/panel/xychart/README.md
Normal file
4
public/app/plugins/panel/xychart/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# XY Chart - Native Plugin
|
||||||
|
|
||||||
|
Support arbitrary X vs Y in graph
|
||||||
|
|
59
public/app/plugins/panel/xychart/XYChartPanel.tsx
Normal file
59
public/app/plugins/panel/xychart/XYChartPanel.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Button, TooltipPlugin, GraphNG, GraphNGLegendEvent } from '@grafana/ui';
|
||||||
|
import { PanelProps } from '@grafana/data';
|
||||||
|
import { Options } from './types';
|
||||||
|
import { hideSeriesConfigFactory } from '../timeseries/hideSeriesConfigFactory';
|
||||||
|
import { getXYDimensions } from './dims';
|
||||||
|
|
||||||
|
interface XYChartPanelProps extends PanelProps<Options> {}
|
||||||
|
|
||||||
|
export const XYChartPanel: React.FC<XYChartPanelProps> = ({
|
||||||
|
data,
|
||||||
|
timeRange,
|
||||||
|
timeZone,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
options,
|
||||||
|
fieldConfig,
|
||||||
|
onFieldConfigChange,
|
||||||
|
}) => {
|
||||||
|
const dims = useMemo(() => getXYDimensions(options.dims, data.series), [options.dims, data.series]);
|
||||||
|
if (dims.error) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>ERROR: {dims.error}</div>
|
||||||
|
{dims.hasData && (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => alert('TODO, switch vis')}>Show as Table</Button>
|
||||||
|
{dims.hasTime && <Button onClick={() => alert('TODO, switch vis')}>Show as Time series</Button>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const frames = useMemo(() => [dims.frame], [dims]);
|
||||||
|
|
||||||
|
const onLegendClick = useCallback(
|
||||||
|
(event: GraphNGLegendEvent) => {
|
||||||
|
onFieldConfigChange(hideSeriesConfigFactory(event, fieldConfig, frames));
|
||||||
|
},
|
||||||
|
[fieldConfig, onFieldConfigChange, frames]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GraphNG
|
||||||
|
data={frames}
|
||||||
|
fields={dims.fields}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={timeZone}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
legend={options.legend}
|
||||||
|
onLegendClick={onLegendClick}
|
||||||
|
>
|
||||||
|
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
|
||||||
|
<>{/* needs to be an array */}</>
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
};
|
177
public/app/plugins/panel/xychart/XYDimsEditor.tsx
Normal file
177
public/app/plugins/panel/xychart/XYDimsEditor.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { IconButton, Label, Select, stylesFactory, Switch, useTheme } from '@grafana/ui';
|
||||||
|
import {
|
||||||
|
SelectableValue,
|
||||||
|
getFrameDisplayName,
|
||||||
|
GrafanaTheme,
|
||||||
|
StandardEditorProps,
|
||||||
|
getFieldDisplayName,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
|
import { XYDimensionConfig, Options } from './types';
|
||||||
|
import { getXYDimensions, isGraphable } from './dims';
|
||||||
|
|
||||||
|
interface XYInfo {
|
||||||
|
numberFields: Array<SelectableValue<string>>;
|
||||||
|
xAxis: SelectableValue<string>;
|
||||||
|
yFields: Array<SelectableValue<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, Options>> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
if (!context.data) {
|
||||||
|
return <div>No data...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameNames = useMemo(() => {
|
||||||
|
if (context.data && context.data.length > 0) {
|
||||||
|
return context.data.map((f, idx) => ({
|
||||||
|
value: idx,
|
||||||
|
label: getFrameDisplayName(f, idx),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [{ value: 0, label: 'First result' }];
|
||||||
|
}, [context.data, value?.frame]);
|
||||||
|
|
||||||
|
const dims = useMemo(() => getXYDimensions(value, context.data), [context.data, value]);
|
||||||
|
|
||||||
|
const info = useMemo(() => {
|
||||||
|
const first = {
|
||||||
|
label: '?',
|
||||||
|
value: undefined, // empty
|
||||||
|
};
|
||||||
|
const v: XYInfo = {
|
||||||
|
numberFields: [first],
|
||||||
|
yFields: [],
|
||||||
|
xAxis: value?.x
|
||||||
|
? {
|
||||||
|
label: `${value.x} (Not found)`,
|
||||||
|
value: value.x, // empty
|
||||||
|
}
|
||||||
|
: first,
|
||||||
|
};
|
||||||
|
const frame = context.data ? context.data[value?.frame ?? 0] : undefined;
|
||||||
|
if (frame) {
|
||||||
|
const xName = getFieldDisplayName(dims.x, dims.frame, context.data);
|
||||||
|
for (let field of frame.fields) {
|
||||||
|
if (isGraphable(field)) {
|
||||||
|
const name = getFieldDisplayName(field, frame, context.data);
|
||||||
|
const sel = {
|
||||||
|
label: name,
|
||||||
|
value: name,
|
||||||
|
};
|
||||||
|
v.numberFields.push(sel);
|
||||||
|
if (first.label === '?') {
|
||||||
|
first.label = `${name} (First)`;
|
||||||
|
}
|
||||||
|
if (value?.x && name === value.x) {
|
||||||
|
v.xAxis = sel;
|
||||||
|
}
|
||||||
|
if (xName !== name) {
|
||||||
|
v.yFields.push({
|
||||||
|
label: name,
|
||||||
|
value: value?.exclude?.includes(name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}, [dims, context.data, value]);
|
||||||
|
|
||||||
|
const toggleSort = useCallback(() => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
sort: !value?.sort,
|
||||||
|
});
|
||||||
|
}, [value, onChange]);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
options={frameNames}
|
||||||
|
value={frameNames.find(v => v.value === value?.frame) ?? frameNames[0]}
|
||||||
|
onChange={v => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
frame: v.value!,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Label>X Field</Label>
|
||||||
|
<Select
|
||||||
|
options={info.numberFields}
|
||||||
|
value={info.xAxis}
|
||||||
|
onChange={v => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
x: v.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={styles.sorter}>
|
||||||
|
<Switch value={value?.sort ?? false} onClick={toggleSort} />
|
||||||
|
<div onClick={toggleSort}> Sort field</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<Label>Y Fields</Label>
|
||||||
|
<div>
|
||||||
|
{info.yFields.map(v => (
|
||||||
|
<div key={v.label} className={styles.row}>
|
||||||
|
<IconButton
|
||||||
|
name={v.value ? 'eye-slash' : 'eye'}
|
||||||
|
onClick={() => {
|
||||||
|
const exclude: string[] = value?.exclude ? [...value.exclude] : [];
|
||||||
|
let idx = exclude.indexOf(v.label!);
|
||||||
|
if (idx < 0) {
|
||||||
|
exclude.push(v.label!);
|
||||||
|
} else {
|
||||||
|
exclude.splice(idx, 1);
|
||||||
|
}
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
exclude,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{v.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<br /> <br />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||||
|
sorter: css`
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
|
||||||
|
row: css`
|
||||||
|
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
||||||
|
border-radius: ${theme.border.radius.sm};
|
||||||
|
background: ${theme.colors.bg2};
|
||||||
|
min-height: ${theme.spacing.formInputHeight}px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
border: 1px solid ${theme.colors.formInputBorder};
|
||||||
|
`,
|
||||||
|
}));
|
103
public/app/plugins/panel/xychart/dims.ts
Normal file
103
public/app/plugins/panel/xychart/dims.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { DataFrame, Field, FieldMatcher, FieldType, getFieldDisplayName, sortDataFrame } from '@grafana/data';
|
||||||
|
import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/GraphNG';
|
||||||
|
import { XYDimensionConfig } from './types';
|
||||||
|
|
||||||
|
export enum DimensionError {
|
||||||
|
NoData,
|
||||||
|
BadFrameSelection,
|
||||||
|
XNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface XYDimensions {
|
||||||
|
frame: DataFrame; // matches order from configs, excluds non-graphable values
|
||||||
|
x: Field;
|
||||||
|
fields: XYFieldMatchers;
|
||||||
|
error?: DimensionError;
|
||||||
|
hasData?: boolean;
|
||||||
|
hasTime?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGraphable(field: Field) {
|
||||||
|
return field.type === FieldType.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getXYDimensions(cfg: XYDimensionConfig, data?: DataFrame[]): XYDimensions {
|
||||||
|
if (!data || !data.length) {
|
||||||
|
return { error: DimensionError.NoData } as XYDimensions;
|
||||||
|
}
|
||||||
|
if (!cfg) {
|
||||||
|
cfg = {
|
||||||
|
frame: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame = data[cfg.frame ?? 0];
|
||||||
|
if (!frame) {
|
||||||
|
return { error: DimensionError.BadFrameSelection } as XYDimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xIndex = -1;
|
||||||
|
for (let i = 0; i < frame.fields.length; i++) {
|
||||||
|
const f = frame.fields[i];
|
||||||
|
if (cfg.x && cfg.x === getFieldDisplayName(f, frame, data)) {
|
||||||
|
xIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isGraphable(f) && !cfg.x) {
|
||||||
|
xIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally sort
|
||||||
|
if (cfg.sort) {
|
||||||
|
frame = sortDataFrame(frame, xIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasTime = false;
|
||||||
|
const x = frame.fields[xIndex];
|
||||||
|
const fields: Field[] = [x];
|
||||||
|
for (const f of frame.fields) {
|
||||||
|
if (f.type === FieldType.time) {
|
||||||
|
hasTime = true;
|
||||||
|
}
|
||||||
|
if (f === x || !isGraphable(f)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cfg.exclude) {
|
||||||
|
const name = getFieldDisplayName(f, frame, data);
|
||||||
|
if (cfg.exclude.includes(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
fields: {
|
||||||
|
x: getSimpleFieldMatcher(x),
|
||||||
|
y: getSimpleFieldNotMatcher(x), // Not x
|
||||||
|
},
|
||||||
|
frame: {
|
||||||
|
...frame,
|
||||||
|
fields,
|
||||||
|
},
|
||||||
|
hasData: frame.fields.length > 0,
|
||||||
|
hasTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSimpleFieldMatcher(f: Field): FieldMatcher {
|
||||||
|
if (!f) {
|
||||||
|
return () => false;
|
||||||
|
}
|
||||||
|
return field => f === field;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSimpleFieldNotMatcher(f: Field): FieldMatcher {
|
||||||
|
if (!f) {
|
||||||
|
return () => false;
|
||||||
|
}
|
||||||
|
return field => f !== field;
|
||||||
|
}
|
1
public/app/plugins/panel/xychart/img/icn-xychart.svg
Normal file
1
public/app/plugins/panel/xychart/img/icn-xychart.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 82.59 82.5"><defs><style>.cls-1{fill:#3865ab;}.cls-2{fill:#84aff1;}.cls-3{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="21.17" x2="82.59" y2="21.17" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><rect class="cls-1" x="73.22" y="19.61" width="8" height="62.89" rx="1"/><rect class="cls-1" x="1.78" y="53.61" width="8" height="28.89" rx="1"/><path class="cls-2" d="M8.78,82.5h-6a1,1,0,0,1-1-1V71.61h8V81.5A1,1,0,0,1,8.78,82.5Z"/><path class="cls-2" d="M80.22,82.5h-6a1,1,0,0,1-1-1V46.61h8V81.5A1,1,0,0,1,80.22,82.5Z"/><rect class="cls-1" x="58.93" y="49.61" width="8" height="32.89" rx="1"/><path class="cls-2" d="M65.93,82.5h-6a1,1,0,0,1-1-1V64.61h8V81.5A1,1,0,0,1,65.93,82.5Z"/><rect class="cls-1" x="44.64" y="38.61" width="8" height="43.89" rx="1"/><path class="cls-2" d="M51.64,82.5h-6a1,1,0,0,1-1-1V75.61h8V81.5A1,1,0,0,1,51.64,82.5Z"/><rect class="cls-1" x="30.36" y="27.61" width="8" height="54.89" rx="1"/><path class="cls-2" d="M37.36,82.5h-6a1,1,0,0,1-1-1V42.61h8V81.5A1,1,0,0,1,37.36,82.5Z"/><rect class="cls-1" x="16.07" y="37.61" width="8" height="44.89" rx="1"/><path class="cls-2" d="M23.07,82.5h-6a1,1,0,0,1-1-1V55.61h8V81.5A1,1,0,0,1,23.07,82.5Z"/><path class="cls-3" d="M2,42.33a2,2,0,0,1-1.44-.61,2,2,0,0,1,0-2.83l26-25a2,2,0,0,1,2.2-.39L54.56,25,79.18.58A2,2,0,0,1,82,3.42L56.41,28.75a2,2,0,0,1-2.22.41L28.42,17.71l-25,24.06A2,2,0,0,1,2,42.33Z"/></g></g></svg>
|
After Width: | Height: | Size: 1.6 KiB |
62
public/app/plugins/panel/xychart/module.tsx
Normal file
62
public/app/plugins/panel/xychart/module.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { PanelPlugin } from '@grafana/data';
|
||||||
|
import { DrawStyle, GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
|
||||||
|
import { XYChartPanel } from './XYChartPanel';
|
||||||
|
import { Options } from './types';
|
||||||
|
import { XYDimsEditor } from './XYDimsEditor';
|
||||||
|
import { getGraphFieldConfig, defaultGraphConfig } from '../timeseries/config';
|
||||||
|
|
||||||
|
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(XYChartPanel)
|
||||||
|
.useFieldConfig(
|
||||||
|
getGraphFieldConfig({
|
||||||
|
...defaultGraphConfig,
|
||||||
|
drawStyle: DrawStyle.Points,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.setPanelOptions(builder => {
|
||||||
|
builder
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'xyPlotConfig',
|
||||||
|
path: 'dims',
|
||||||
|
name: 'Data',
|
||||||
|
editor: XYDimsEditor,
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'tooltipOptions.mode',
|
||||||
|
name: 'Tooltip mode',
|
||||||
|
description: '',
|
||||||
|
defaultValue: 'single',
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ value: 'single', label: 'Single' },
|
||||||
|
{ value: 'multi', label: 'All' },
|
||||||
|
{ value: 'none', label: 'Hidden' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'legend.displayMode',
|
||||||
|
name: 'Legend mode',
|
||||||
|
description: '',
|
||||||
|
defaultValue: LegendDisplayMode.List,
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ value: LegendDisplayMode.List, label: 'List' },
|
||||||
|
{ value: LegendDisplayMode.Table, label: 'Table' },
|
||||||
|
{ value: LegendDisplayMode.Hidden, label: 'Hidden' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'legend.placement',
|
||||||
|
name: 'Legend placement',
|
||||||
|
description: '',
|
||||||
|
defaultValue: 'bottom',
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ value: 'bottom', label: 'Bottom' },
|
||||||
|
{ value: 'right', label: 'Right' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
showIf: c => c.legend.displayMode !== LegendDisplayMode.Hidden,
|
||||||
|
});
|
||||||
|
});
|
17
public/app/plugins/panel/xychart/plugin.json
Normal file
17
public/app/plugins/panel/xychart/plugin.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"type": "panel",
|
||||||
|
"name": "XY Chart",
|
||||||
|
"id": "xychart",
|
||||||
|
"state": "alpha",
|
||||||
|
|
||||||
|
"info": {
|
||||||
|
"author": {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "https://grafana.com"
|
||||||
|
},
|
||||||
|
"logos": {
|
||||||
|
"small": "img/icn-xychart.svg",
|
||||||
|
"large": "img/icn-xychart.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
public/app/plugins/panel/xychart/types.ts
Normal file
14
public/app/plugins/panel/xychart/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { LegendOptions, GraphTooltipOptions } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface XYDimensionConfig {
|
||||||
|
frame: number;
|
||||||
|
sort?: boolean;
|
||||||
|
x?: string; // name | first
|
||||||
|
exclude?: string[]; // all other numbers except
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
dims: XYDimensionConfig;
|
||||||
|
legend: LegendOptions;
|
||||||
|
tooltipOptions: GraphTooltipOptions;
|
||||||
|
}
|
Reference in New Issue
Block a user