Timeline: legend (#34340)

This commit is contained in:
Leon Sorokin 2021-05-19 23:38:31 -05:00 committed by GitHub
parent 488529b99f
commit 9237348076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 21 deletions

View File

@ -15,6 +15,7 @@
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 329,
"links": [],
"panels": [
{
@ -53,6 +54,7 @@
},
"id": 8,
"options": {
"alignValue": "left",
"colWidth": 0.9,
"legend": {
"calcs": [],
@ -230,6 +232,7 @@
},
"id": 9,
"options": {
"alignValue": "left",
"colWidth": 0.9,
"legend": {
"calcs": [],
@ -268,7 +271,7 @@
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
"mode": "thresholds"
},
"custom": {
"fillOpacity": 70,
@ -276,12 +279,16 @@
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 60
},
{
"color": "red",
"value": 80
@ -364,5 +371,5 @@
"timezone": "utc",
"title": "Timeline Modes",
"uid": "mIJjFy8Gz",
"version": 21
}
"version": 12
}

View File

@ -5,6 +5,7 @@ export enum FieldColorModeId {
Thresholds = 'thresholds',
PaletteClassic = 'palette-classic',
PaletteSaturated = 'palette-saturated',
ContinuousGrYlRd = 'continuous-GrYlRd',
Fixed = 'fixed',
}

View File

@ -3,7 +3,7 @@ import { PanelProps } from '@grafana/data';
import { useTheme2, ZoomPlugin } from '@grafana/ui';
import { TimelineMode, TimelineOptions } from './types';
import { TimelineChart } from './TimelineChart';
import { prepareTimelineFields } from './utils';
import { prepareTimelineFields, prepareTimelineLegendItems } from './utils';
interface TimelinePanelProps extends PanelProps<TimelineOptions> {}
@ -26,6 +26,8 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
options.mergeValues,
]);
const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend), [frames, options.legend]);
if (!frames || warn) {
return (
<div className="panel-empty">
@ -43,6 +45,7 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
timeZone={timeZone}
width={width}
height={height}
legendItems={legendItems}
{...options}
// hardcoded
mode={TimelineMode.Changes}

View File

@ -1,5 +1,16 @@
import React from 'react';
import { PanelContext, PanelContextRoot, GraphNG, GraphNGProps, BarValueVisibility } from '@grafana/ui';
import {
PanelContext,
PanelContextRoot,
GraphNG,
GraphNGProps,
BarValueVisibility,
LegendDisplayMode,
UPlotConfigBuilder,
VizLayout,
VizLegend,
VizLegendItem,
} from '@grafana/ui';
import { DataFrame, FieldType, TimeRange } from '@grafana/data';
import { preparePlotConfigBuilder } from './utils';
import { TimelineMode, TimelineValueAlignment } from './types';
@ -13,6 +24,7 @@ export interface TimelineProps extends Omit<GraphNGProps, 'prepConfig' | 'propsT
showValue: BarValueVisibility;
alignValue?: TimelineValueAlignment;
colWidth?: number;
legendItems?: VizLegendItem[];
}
const propsToDiff = ['rowHeight', 'colWidth', 'showValue', 'mergeValues', 'alignValue'];
@ -33,7 +45,19 @@ export class TimelineChart extends React.Component<TimelineProps> {
});
};
renderLegend = () => null;
renderLegend = (config: UPlotConfigBuilder) => {
const { legend, legendItems } = this.props;
if (!config || !legendItems || !legend || legend.displayMode === LegendDisplayMode.Hidden) {
return null;
}
return (
<VizLayout.Legend placement={legend.placement}>
<VizLegend placement={legend.placement} items={legendItems} displayMode={legend.displayMode} />
</VizLayout.Legend>
);
};
render() {
return (

View File

@ -2,6 +2,7 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/dat
import { StateTimelinePanel } from './StateTimelinePanel';
import { TimelineOptions, TimelineFieldConfig, defaultPanelOptions, defaultTimelineFieldConfig } from './types';
import { BarValueVisibility } from '@grafana/ui';
import { addLegendOptions } from '@grafana/ui/src/options/builder';
export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(StateTimelinePanel)
.useFieldConfig({
@ -11,7 +12,7 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Stat
byValueSupport: true,
},
defaultValue: {
mode: FieldColorModeId.PaletteClassic,
mode: FieldColorModeId.ContinuousGrYlRd,
},
},
},
@ -81,5 +82,5 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Stat
defaultValue: defaultPanelOptions.rowHeight,
});
//addLegendOptions(builder);
addLegendOptions(builder, false);
});

View File

@ -3,7 +3,7 @@
"name": "State timeline",
"id": "state-timeline",
"state": "alpha",
"state": "beta",
"info": {
"description": "State changes and durations",

View File

@ -1,12 +1,11 @@
import { VizLegendOptions, HideableFieldConfig, BarValueVisibility } from '@grafana/ui';
import { HideableFieldConfig, BarValueVisibility, OptionsWithLegend } from '@grafana/ui';
/**
* @alpha
*/
export interface TimelineOptions {
export interface TimelineOptions extends OptionsWithLegend {
mode: TimelineMode; // not in the saved model!
legend: VizLegendOptions;
showValue: BarValueVisibility;
rowHeight: number;

View File

@ -10,8 +10,18 @@ import {
FALLBACK_COLOR,
FieldType,
ArrayVector,
FieldColorModeId,
getValueFormat,
ThresholdsMode,
} from '@grafana/data';
import { UPlotConfigBuilder, FIXED_UNIT, SeriesVisibilityChangeMode, UPlotConfigPrepFn } from '@grafana/ui';
import {
UPlotConfigBuilder,
FIXED_UNIT,
SeriesVisibilityChangeMode,
UPlotConfigPrepFn,
VizLegendOptions,
VizLegendItem,
} from '@grafana/ui';
import { TimelineCoreOptions, getConfig } from './timeline';
import { AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/ui/src/components/uPlot/config';
import { measureText } from '@grafana/ui/src/utils/measureText';
@ -291,3 +301,77 @@ export function prepareTimelineFields(
}
return { frames };
}
export function prepareTimelineLegendItems(
frames: DataFrame[] | undefined,
options: VizLegendOptions
): VizLegendItem[] | undefined {
if (!frames || options.displayMode === 'hidden') {
return undefined;
}
const fields = allNonTimeFields(frames);
if (!fields.length) {
return undefined;
}
const items: VizLegendItem[] = [];
const first = fields[0].config;
const colorMode = first.color?.mode ?? FieldColorModeId.Fixed;
// If thresholds are enabled show each step in the legend
if (colorMode === FieldColorModeId.Thresholds && first.thresholds?.steps) {
const steps = first.thresholds.steps;
const disp = getValueFormat(
first.thresholds.mode === ThresholdsMode.Percentage ? 'percent' : first.unit ?? 'fixed'
);
const fmt = (v: number) => formattedValueToString(disp(v));
for (let i = 1; i <= steps.length; i++) {
const step = steps[i - 1];
items.push({
label: i === 1 ? `< ${fmt(steps[i].value)}` : `${fmt(step.value)}+`,
color: step.color,
yAxis: 1,
});
}
return items;
}
// If thresholds are enabled show each step in the legend
if (colorMode.startsWith('continuous')) {
return undefined; // eventually a color bar
}
let stateColors: Map<string, string | undefined> = new Map();
fields.forEach((field) => {
field.values.toArray().forEach((v) => {
let state = field.display!(v);
stateColors.set(state.text, state.color!);
});
});
stateColors.forEach((color, label) => {
if (label.length > 0) {
items.push({
label: label!,
color,
yAxis: 1,
});
}
});
return items;
}
function allNonTimeFields(frames: DataFrame[]): Field[] {
const fields: Field[] = [];
for (const frame of frames) {
for (const field of frame.fields) {
if (field.type !== FieldType.time) {
fields.push(field);
}
}
}
return fields;
}

View File

@ -1,9 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import { PanelProps } from '@grafana/data';
import { useTheme2, ZoomPlugin } from '@grafana/ui';
import { StatusPanelOptions } from './types';
import { TimelineChart } from '../state-timeline/TimelineChart';
import { TimelineMode } from '../state-timeline/types';
import { prepareTimelineFields, prepareTimelineLegendItems } from '../state-timeline/utils';
interface TimelinePanelProps extends PanelProps<StatusPanelOptions> {}
@ -21,10 +22,14 @@ export const StatusGridPanel: React.FC<TimelinePanelProps> = ({
}) => {
const theme = useTheme2();
if (!data || !data.series?.length) {
const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, false), [data]);
const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend), [frames, options.legend]);
if (!frames || warn) {
return (
<div className="panel-empty">
<p>No data found in response</p>
<p>{warn ?? 'No data found in response'}</p>
</div>
);
}
@ -32,12 +37,13 @@ export const StatusGridPanel: React.FC<TimelinePanelProps> = ({
return (
<TimelineChart
theme={theme}
frames={data.series}
frames={frames}
structureRev={data.structureRev}
timeRange={timeRange}
timeZone={timeZone}
width={width}
height={height}
legendItems={legendItems}
{...options}
// hardcoded
mode={TimelineMode.Samples}

View File

@ -2,6 +2,7 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/dat
import { StatusGridPanel } from './StatusGridPanel';
import { StatusPanelOptions, StatusFieldConfig, defaultStatusFieldConfig } from './types';
import { BarValueVisibility } from '@grafana/ui';
import { addLegendOptions } from '@grafana/ui/src/options/builder';
export const plugin = new PanelPlugin<StatusPanelOptions, StatusFieldConfig>(StatusGridPanel)
.useFieldConfig({
@ -11,7 +12,7 @@ export const plugin = new PanelPlugin<StatusPanelOptions, StatusFieldConfig>(Sta
byValueSupport: true,
},
defaultValue: {
mode: FieldColorModeId.PaletteClassic,
mode: FieldColorModeId.Thresholds,
},
},
},
@ -74,5 +75,5 @@ export const plugin = new PanelPlugin<StatusPanelOptions, StatusFieldConfig>(Sta
},
});
//addLegendOptions(builder);
addLegendOptions(builder, false);
});

View File

@ -3,7 +3,7 @@
"name": "Status grid",
"id": "status-grid",
"state": "alpha",
"state": "beta",
"info": {
"description": "Periodic status history",