Commit 549587a3 by Ryan McKinley Committed by GitHub

GraphNG: add new alpha XY Chart (#30096)

parent b2d54669
......@@ -6,7 +6,7 @@ import { LiveChannelSupport } from './live';
export enum PluginState {
alpha = 'alpha', // Only included if `enable_alpha` config option is true
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
}
......
......@@ -70,16 +70,16 @@ export interface LineConfig {
/**
* @alpha
*/
export interface AreaConfig {
export interface FillConfig {
fillColor?: string;
fillOpacity?: number;
fillGradient?: AreaGradientMode;
fillGradient?: FillGradientMode;
}
/**
* @alpha
*/
export enum AreaGradientMode {
export enum FillGradientMode {
None = 'none',
Opacity = 'opacity',
Hue = 'hue',
......@@ -126,7 +126,7 @@ export interface HideSeriesConfig {
/**
* @alpha
*/
export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig, AxisConfig {
export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig {
drawStyle?: DrawStyle;
hideFrom?: HideSeriesConfig;
}
......@@ -162,8 +162,8 @@ export const graphFieldOptions = {
] as Array<SelectableValue<AxisPlacement>>,
fillGradient: [
{ label: 'None', value: AreaGradientMode.None },
{ label: 'Opacity', value: AreaGradientMode.Opacity },
{ label: 'Hue', value: AreaGradientMode.Hue },
] as Array<SelectableValue<AreaGradientMode>>,
{ label: 'None', value: FillGradientMode.None },
{ label: 'Opacity', value: FillGradientMode.Opacity },
{ label: 'Hue', value: FillGradientMode.Hue },
] as Array<SelectableValue<FillGradientMode>>,
};
......@@ -3,7 +3,7 @@
import { UPlotConfigBuilder } from './UPlotConfigBuilder';
import { GrafanaTheme } from '@grafana/data';
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('default config', () => {
......@@ -352,7 +352,7 @@ describe('UPlotConfigBuilder', () => {
scaleKey: 'scale-x',
lineColor: '#FFAABB',
fillOpacity: 50,
fillGradient: AreaGradientMode.Opacity,
fillGradient: FillGradientMode.Opacity,
});
expect(builder.getConfig().series[1].fill).toBeInstanceOf(Function);
......@@ -364,7 +364,7 @@ describe('UPlotConfigBuilder', () => {
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
fillOpacity: 50,
fillGradient: AreaGradientMode.Opacity,
fillGradient: FillGradientMode.Opacity,
showPoints: PointVisibility.Auto,
pointSize: 5,
pointColor: '#00ff00',
......
......@@ -4,15 +4,15 @@ import { getCanvasContext } from '../../../utils/measureText';
import {
DrawStyle,
LineConfig,
AreaConfig,
FillConfig,
PointsConfig,
PointVisibility,
LineInterpolation,
AreaGradientMode,
FillGradientMode,
} from '../config';
import { PlotConfigBuilder } from '../types';
export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig {
export interface SeriesProps extends LineConfig, FillConfig, PointsConfig {
drawStyle: DrawStyle;
scaleKey: string;
show?: boolean;
......@@ -89,10 +89,10 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
return fillColor;
}
const mode = fillGradient ?? AreaGradientMode.None;
const mode = fillGradient ?? FillGradientMode.None;
let fillOpacityNumber = fillOpacity ?? 0;
if (mode !== AreaGradientMode.None) {
if (mode !== FillGradientMode.None) {
return getCanvasGradient({
color: (fillColor ?? lineColor)!,
opacity: fillOpacityNumber / 100,
......@@ -160,7 +160,7 @@ function mapDrawStyleToPathBuilder(
interface AreaGradientOptions {
color: string;
mode: AreaGradientMode;
mode: FillGradientMode;
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);
switch (mode) {
case AreaGradientMode.Hue:
case FillGradientMode.Hue:
const color1 = tinycolor(color)
.spin(-25)
.darken(30)
......@@ -186,7 +186,7 @@ function getCanvasGradient(opts: AreaGradientOptions): (self: uPlot, seriesIdx:
gradient.addColorStop(0, color2);
gradient.addColorStop(1, color1);
case AreaGradientMode.Opacity:
case FillGradientMode.Opacity:
default:
gradient.addColorStop(
0,
......
......@@ -41,6 +41,7 @@ const tempoPlugin = async () =>
import * as textPanel from 'app/plugins/panel/text/module';
import * as timeseriesPanel from 'app/plugins/panel/timeseries/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 pluginsListPanel from 'app/plugins/panel/pluginlist/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/timeseries/module': timeseriesPanel,
'app/plugins/panel/graph/module': graphPanel,
'app/plugins/panel/xychart/module': xyChartPanel,
'app/plugins/panel/dashlist/module': dashListPanel,
'app/plugins/panel/pluginlist/module': pluginsListPanel,
'app/plugins/panel/alertlist/module': alertListPanel,
......
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';
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
import {
AreaGradientMode,
FillGradientMode,
AxisPlacement,
DrawStyle,
LineInterpolation,
......@@ -234,7 +234,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
}
if (isNumber(angular.fillGradient) && angular.fillGradient > 0) {
graph.fillGradient = AreaGradientMode.Opacity;
graph.fillGradient = FillGradientMode.Opacity;
graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
}
......
import {
FieldColorModeId,
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 { PanelPlugin } from '@grafana/data';
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
import { TimeSeriesPanel } from './TimeSeriesPanel';
import { graphPanelChangedHandler } from './migrations';
import { Options } from './types';
import { ScaleDistributionEditor } from './ScaleDistributionEditor';
import { LineStyleEditor } from './LineStyleEditor';
import { getGraphFieldConfig, defaultGraphConfig } from './config';
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(TimeSeriesPanel)
.setPanelChangeHandler(graphPanelChangedHandler)
.useFieldConfig({
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,
});
},
})
.useFieldConfig(getGraphFieldConfig(defaultGraphConfig))
.setPanelOptions(builder => {
builder
.addRadio({
......
# XY Chart - Native Plugin
Support arbitrary X vs Y in graph
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>
);
};
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}>&nbsp; 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};
`,
}));
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;
}
<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>
\ No newline at end of file
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,
});
});
{
"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"
}
}
}
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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment