mirror of https://github.com/grafana/grafana
GraphNG: refactor core to class component (#30941)
* First attempt * Get rid of time range as config invalidation dependency * GraphNG class refactor * Get rid of DataFrame dependency from Plot component, get rid of usePlotData context, rely on XYMatchers for data inspection from within plugins * Bring back legend * Fix Sparkline * Fix Sparkline * Sparkline update * Explore update * fix * BarChart refactor to class * Tweaks * TS fix * Fix tests * Tests * Update packages/grafana-ui/src/components/uPlot/utils.ts * Update public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx * GraphNG: unified legend for BarChart, GraphNG & other uPlot based visualizations (#31175) * Legend experiment * Nitspull/31225/head
parent
f9a293afea
commit
9c08b34e71
@ -1,318 +1,138 @@ |
||||
import React, { useCallback, useMemo, useRef } from 'react'; |
||||
import { |
||||
compareDataFrameStructures, |
||||
DataFrame, |
||||
DefaultTimeZone, |
||||
formattedValueToString, |
||||
getFieldDisplayName, |
||||
getFieldSeriesColor, |
||||
getFieldColorModeForField, |
||||
TimeRange, |
||||
VizOrientation, |
||||
fieldReducers, |
||||
reduceField, |
||||
DisplayValue, |
||||
} from '@grafana/data'; |
||||
|
||||
import React from 'react'; |
||||
import { AlignedData } from 'uplot'; |
||||
import { compareArrayValues, compareDataFrameStructures, DataFrame, TimeRange } from '@grafana/data'; |
||||
import { VizLayout } from '../VizLayout/VizLayout'; |
||||
import { Themeable } from '../../types'; |
||||
import { useRevision } from '../uPlot/hooks'; |
||||
import { UPlotChart } from '../uPlot/Plot'; |
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; |
||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '../uPlot/config'; |
||||
import { useTheme } from '../../themes'; |
||||
import { GraphNGLegendEvent, GraphNGLegendEventMode } from '../GraphNG/types'; |
||||
import { FIXED_UNIT } from '../GraphNG/GraphNG'; |
||||
import { LegendDisplayMode, VizLegendItem } from '../VizLegend/types'; |
||||
import { VizLegend } from '../VizLegend/VizLegend'; |
||||
|
||||
import { BarChartFieldConfig, BarChartOptions, BarValueVisibility, defaultBarChartFieldConfig } from './types'; |
||||
import { BarsOptions, getConfig } from './bars'; |
||||
import { GraphNGLegendEvent } from '../GraphNG/types'; |
||||
import { BarChartOptions } from './types'; |
||||
import { withTheme } from '../../themes'; |
||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; |
||||
import { preparePlotData } from '../uPlot/utils'; |
||||
import { LegendDisplayMode } from '../VizLegend/types'; |
||||
import { PlotLegend } from '../uPlot/PlotLegend'; |
||||
|
||||
/** |
||||
* @alpha |
||||
*/ |
||||
export interface Props extends Themeable, BarChartOptions { |
||||
export interface BarChartProps extends Themeable, BarChartOptions { |
||||
height: number; |
||||
width: number; |
||||
data: DataFrame; |
||||
data: DataFrame[]; |
||||
onLegendClick?: (event: GraphNGLegendEvent) => void; |
||||
onSeriesColorChange?: (label: string, color: string) => void; |
||||
} |
||||
|
||||
/** |
||||
* @alpha |
||||
*/ |
||||
export const BarChart: React.FunctionComponent<Props> = ({ |
||||
width, |
||||
height, |
||||
data, |
||||
orientation, |
||||
groupWidth, |
||||
barWidth, |
||||
showValue, |
||||
legend, |
||||
onLegendClick, |
||||
onSeriesColorChange, |
||||
...plotProps |
||||
}) => { |
||||
if (!data || data.fields.length < 2) { |
||||
return <div>Missing data</div>; |
||||
interface BarChartState { |
||||
data: AlignedData; |
||||
alignedDataFrame: DataFrame; |
||||
config?: UPlotConfigBuilder; |
||||
} |
||||
|
||||
// dominik? TODO? can this all be moved into `useRevision`
|
||||
const compareFrames = useCallback((a?: DataFrame | null, b?: DataFrame | null) => { |
||||
if (a && b) { |
||||
return compareDataFrameStructures(a, b); |
||||
class UnthemedBarChart extends React.Component<BarChartProps, BarChartState> { |
||||
constructor(props: BarChartProps) { |
||||
super(props); |
||||
this.state = {} as BarChartState; |
||||
} |
||||
return false; |
||||
}, []); |
||||
|
||||
const configRev = useRevision(data, compareFrames); |
||||
|
||||
const theme = useTheme(); |
||||
static getDerivedStateFromProps(props: BarChartProps, state: BarChartState) { |
||||
const frame = preparePlotFrame(props.data); |
||||
|
||||
// Updates only when the structure changes
|
||||
const configBuilder = useMemo(() => { |
||||
if (!orientation || orientation === VizOrientation.Auto) { |
||||
orientation = width < height ? VizOrientation.Horizontal : VizOrientation.Vertical; |
||||
if (!frame) { |
||||
return { ...state }; |
||||
} |
||||
|
||||
// bar orientation -> x scale orientation & direction
|
||||
let xOri: ScaleOrientation, xDir: ScaleDirection, yOri: ScaleOrientation, yDir: ScaleDirection; |
||||
|
||||
if (orientation === VizOrientation.Vertical) { |
||||
xOri = ScaleOrientation.Horizontal; |
||||
xDir = ScaleDirection.Right; |
||||
yOri = ScaleOrientation.Vertical; |
||||
yDir = ScaleDirection.Up; |
||||
} else { |
||||
xOri = ScaleOrientation.Vertical; |
||||
xDir = ScaleDirection.Down; |
||||
yOri = ScaleOrientation.Horizontal; |
||||
yDir = ScaleDirection.Right; |
||||
return { |
||||
...state, |
||||
data: preparePlotData(frame), |
||||
alignedDataFrame: frame, |
||||
}; |
||||
} |
||||
|
||||
const formatValue = |
||||
showValue !== BarValueVisibility.Never |
||||
? (seriesIdx: number, value: any) => formattedValueToString(data.fields[seriesIdx].display!(value)) |
||||
: undefined; |
||||
componentDidMount() { |
||||
const { alignedDataFrame } = this.state; |
||||
|
||||
// Use bar width when only one field
|
||||
if (data.fields.length === 2) { |
||||
groupWidth = barWidth; |
||||
barWidth = 1; |
||||
if (!alignedDataFrame) { |
||||
return; |
||||
} |
||||
|
||||
const opts: BarsOptions = { |
||||
xOri, |
||||
xDir, |
||||
groupWidth, |
||||
barWidth, |
||||
formatValue, |
||||
onHover: (seriesIdx: number, valueIdx: number) => { |
||||
console.log('hover', { seriesIdx, valueIdx }); |
||||
}, |
||||
onLeave: (seriesIdx: number, valueIdx: number) => { |
||||
console.log('leave', { seriesIdx, valueIdx }); |
||||
}, |
||||
}; |
||||
const config = getConfig(opts); |
||||
|
||||
const builder = new UPlotConfigBuilder(); |
||||
|
||||
builder.addHook('init', config.init); |
||||
builder.addHook('drawClear', config.drawClear); |
||||
builder.addHook('setCursor', config.setCursor); |
||||
|
||||
builder.setCursor(config.cursor); |
||||
builder.setSelect(config.select); |
||||
|
||||
builder.addScale({ |
||||
scaleKey: 'x', |
||||
isTime: false, |
||||
distribution: ScaleDistribution.Ordinal, |
||||
orientation: xOri, |
||||
direction: xDir, |
||||
this.setState({ |
||||
config: preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props), |
||||
}); |
||||
} |
||||
|
||||
builder.addAxis({ |
||||
scaleKey: 'x', |
||||
isTime: false, |
||||
placement: xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left, |
||||
splits: config.xSplits, |
||||
values: config.xValues, |
||||
grid: false, |
||||
ticks: false, |
||||
gap: 15, |
||||
theme, |
||||
}); |
||||
|
||||
let seriesIndex = 0; |
||||
|
||||
// iterate the y values
|
||||
for (let i = 1; i < data.fields.length; i++) { |
||||
const field = data.fields[i]; |
||||
|
||||
field.state!.seriesIndex = seriesIndex++; |
||||
|
||||
const customConfig: BarChartFieldConfig = { ...defaultBarChartFieldConfig, ...field.config.custom }; |
||||
|
||||
const scaleKey = field.config.unit || FIXED_UNIT; |
||||
const colorMode = getFieldColorModeForField(field); |
||||
const scaleColor = getFieldSeriesColor(field, theme); |
||||
const seriesColor = scaleColor.color; |
||||
|
||||
builder.addSeries({ |
||||
scaleKey, |
||||
pxAlign: false, |
||||
lineWidth: customConfig.lineWidth, |
||||
lineColor: seriesColor, |
||||
//lineStyle: customConfig.lineStyle,
|
||||
fillOpacity: customConfig.fillOpacity, |
||||
theme, |
||||
colorMode, |
||||
pathBuilder: config.drawBars, |
||||
pointsBuilder: config.drawPoints, |
||||
show: !customConfig.hideFrom?.graph, |
||||
gradientMode: customConfig.gradientMode, |
||||
thresholds: field.config.thresholds, |
||||
|
||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||||
dataFrameFieldIndex: { |
||||
fieldIndex: i, |
||||
frameIndex: 0, |
||||
}, |
||||
fieldName: getFieldDisplayName(field, data), |
||||
hideInLegend: customConfig.hideFrom?.legend, |
||||
}); |
||||
componentDidUpdate(prevProps: BarChartProps) { |
||||
const { data, orientation, groupWidth, barWidth, showValue } = this.props; |
||||
const { alignedDataFrame } = this.state; |
||||
let shouldConfigUpdate = false; |
||||
let hasStructureChanged = false; |
||||
|
||||
// The builder will manage unique scaleKeys and combine where appropriate
|
||||
builder.addScale({ |
||||
scaleKey, |
||||
min: field.config.min, |
||||
max: field.config.max, |
||||
softMin: customConfig.axisSoftMin, |
||||
softMax: customConfig.axisSoftMax, |
||||
orientation: yOri, |
||||
direction: yDir, |
||||
}); |
||||
|
||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) { |
||||
let placement = customConfig.axisPlacement; |
||||
if (!placement || placement === AxisPlacement.Auto) { |
||||
placement = AxisPlacement.Left; |
||||
} |
||||
if (xOri === 1) { |
||||
if (placement === AxisPlacement.Left) { |
||||
placement = AxisPlacement.Bottom; |
||||
if ( |
||||
this.state.config === undefined || |
||||
orientation !== prevProps.orientation || |
||||
groupWidth !== prevProps.groupWidth || |
||||
barWidth !== prevProps.barWidth || |
||||
showValue !== prevProps.showValue |
||||
) { |
||||
shouldConfigUpdate = true; |
||||
} |
||||
if (placement === AxisPlacement.Right) { |
||||
placement = AxisPlacement.Top; |
||||
|
||||
if (data !== prevProps.data) { |
||||
if (!alignedDataFrame) { |
||||
return; |
||||
} |
||||
hasStructureChanged = !compareArrayValues(data, prevProps.data, compareDataFrameStructures); |
||||
} |
||||
|
||||
builder.addAxis({ |
||||
scaleKey, |
||||
label: customConfig.axisLabel, |
||||
size: customConfig.axisWidth, |
||||
placement, |
||||
formatValue: (v) => formattedValueToString(field.display!(v)), |
||||
theme, |
||||
if (shouldConfigUpdate || hasStructureChanged) { |
||||
this.setState({ |
||||
config: preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props), |
||||
}); |
||||
} |
||||
} |
||||
|
||||
return builder; |
||||
}, [data, configRev, orientation, width, height]); |
||||
|
||||
const onLabelClick = useCallback( |
||||
(legend: VizLegendItem, event: React.MouseEvent) => { |
||||
const { fieldIndex } = legend; |
||||
renderLegend() { |
||||
const { legend, onSeriesColorChange, onLegendClick, data } = this.props; |
||||
const { config } = this.state; |
||||
|
||||
if (!onLegendClick || !fieldIndex) { |
||||
if (!config || legend.displayMode === LegendDisplayMode.Hidden) { |
||||
return; |
||||
} |
||||
|
||||
onLegendClick({ |
||||
fieldIndex, |
||||
mode: GraphNGLegendEventMode.AppendToSelection, |
||||
}); |
||||
}, |
||||
[onLegendClick, data] |
||||
return ( |
||||
<PlotLegend |
||||
data={data} |
||||
config={config} |
||||
onSeriesColorChange={onSeriesColorChange} |
||||
onLegendClick={onLegendClick} |
||||
{...legend} |
||||
/> |
||||
); |
||||
|
||||
const hasLegend = useRef(legend && legend.displayMode !== LegendDisplayMode.Hidden); |
||||
|
||||
const legendItems = configBuilder |
||||
.getSeries() |
||||
.map<VizLegendItem | undefined>((s) => { |
||||
const seriesConfig = s.props; |
||||
const fieldIndex = seriesConfig.dataFrameFieldIndex; |
||||
if (seriesConfig.hideInLegend || !fieldIndex) { |
||||
return undefined; |
||||
} |
||||
|
||||
const field = data.fields[fieldIndex.fieldIndex]; |
||||
if (!field) { |
||||
return undefined; |
||||
} |
||||
render() { |
||||
const { width, height } = this.props; |
||||
const { config, data } = this.state; |
||||
|
||||
return { |
||||
disabled: !seriesConfig.show ?? false, |
||||
fieldIndex, |
||||
color: seriesConfig.lineColor!, |
||||
label: seriesConfig.fieldName, |
||||
yAxis: 1, |
||||
getDisplayValues: () => { |
||||
if (!legend.calcs?.length) { |
||||
return []; |
||||
} |
||||
|
||||
const fieldCalcs = reduceField({ |
||||
field, |
||||
reducers: legend.calcs, |
||||
}); |
||||
|
||||
return legend.calcs.map<DisplayValue>((reducer) => { |
||||
return { |
||||
...field.display!(fieldCalcs[reducer]), |
||||
title: fieldReducers.get(reducer).name, |
||||
}; |
||||
}); |
||||
}, |
||||
}; |
||||
}) |
||||
.filter((i) => i !== undefined) as VizLegendItem[]; |
||||
|
||||
let legendElement: React.ReactElement | undefined; |
||||
|
||||
if (hasLegend && legendItems.length > 0) { |
||||
legendElement = ( |
||||
<VizLayout.Legend position={legend.placement} maxHeight="35%" maxWidth="60%"> |
||||
<VizLegend |
||||
onLabelClick={onLabelClick} |
||||
placement={legend.placement} |
||||
items={legendItems} |
||||
displayMode={legend.displayMode} |
||||
onSeriesColorChange={onSeriesColorChange} |
||||
/> |
||||
</VizLayout.Legend> |
||||
); |
||||
if (!config) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<VizLayout width={width} height={height} legend={legendElement}> |
||||
<VizLayout width={width} height={height} legend={this.renderLegend()}> |
||||
{(vizWidth: number, vizHeight: number) => ( |
||||
<UPlotChart |
||||
data={data} |
||||
config={configBuilder} |
||||
config={config} |
||||
width={vizWidth} |
||||
height={vizHeight} |
||||
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
||||
timeZone={DefaultTimeZone} |
||||
/> |
||||
)} |
||||
</VizLayout> |
||||
); |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export const BarChart = withTheme(UnthemedBarChart); |
||||
BarChart.displayName = 'GraphNG'; |
||||
|
||||
@ -0,0 +1,961 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder orientation 1`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "bottom", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "left", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": -1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 1, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 0, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder orientation 2`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "bottom", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "left", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": -1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 1, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 0, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder orientation 3`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "left", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "bottom", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 0, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 1, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder stacking 1`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "bottom", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "left", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": -1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 1, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 0, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder stacking 2`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "bottom", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "left", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": -1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 1, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 0, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder stacking 3`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "bottom", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "left", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": -1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 1, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 0, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder value visibility 1`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "bottom", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "left", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": -1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 1, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 0, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder value visibility 2`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"m/s": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "bottom", |
||||
"scaleKey": "m/s", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"gap": 15, |
||||
"grid": false, |
||||
"isTime": false, |
||||
"placement": "left", |
||||
"scaleKey": "x", |
||||
"splits": [Function], |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"ticks": false, |
||||
"values": [Function], |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"cursor": Object { |
||||
"points": Object { |
||||
"show": false, |
||||
}, |
||||
"x": false, |
||||
"y": false, |
||||
}, |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object { |
||||
"drawClear": Array [ |
||||
[Function], |
||||
], |
||||
"init": Array [ |
||||
[Function], |
||||
], |
||||
"setCursor": Array [ |
||||
[Function], |
||||
], |
||||
}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": -1, |
||||
"distribution": "ordinal", |
||||
"isTime": false, |
||||
"orientation": 1, |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 0, |
||||
"scaleKey": "m/s", |
||||
"softMax": undefined, |
||||
"softMin": 0, |
||||
}, |
||||
}, |
||||
], |
||||
"select": Object { |
||||
"show": false, |
||||
}, |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#808080", |
||||
"lineWidth": 2, |
||||
"pathBuilder": [Function], |
||||
"pointsBuilder": [Function], |
||||
"pxAlign": false, |
||||
"scaleKey": "m/s", |
||||
"show": true, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
@ -0,0 +1,101 @@ |
||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; |
||||
import { FieldConfig, FieldType, GrafanaTheme, MutableDataFrame, VizOrientation } from '@grafana/data'; |
||||
import { BarChartFieldConfig, BarChartOptions, BarStackingMode, BarValueVisibility } from './types'; |
||||
import { GraphGradientMode } from '../uPlot/config'; |
||||
import { LegendDisplayMode } from '../VizLegend/types'; |
||||
|
||||
function mockDataFrame() { |
||||
const df1 = new MutableDataFrame({ |
||||
refId: 'A', |
||||
fields: [{ name: 'ts', type: FieldType.string, values: ['a', 'b', 'c'] }], |
||||
}); |
||||
|
||||
const df2 = new MutableDataFrame({ |
||||
refId: 'B', |
||||
fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }], |
||||
}); |
||||
|
||||
const f1Config: FieldConfig<BarChartFieldConfig> = { |
||||
displayName: 'Metric 1', |
||||
decimals: 2, |
||||
unit: 'm/s', |
||||
custom: { |
||||
gradientMode: GraphGradientMode.Opacity, |
||||
lineWidth: 2, |
||||
fillOpacity: 0.1, |
||||
}, |
||||
}; |
||||
|
||||
const f2Config: FieldConfig<BarChartFieldConfig> = { |
||||
displayName: 'Metric 2', |
||||
decimals: 2, |
||||
unit: 'kWh', |
||||
custom: { |
||||
gradientMode: GraphGradientMode.Hue, |
||||
lineWidth: 2, |
||||
fillOpacity: 0.1, |
||||
}, |
||||
}; |
||||
|
||||
df1.addField({ |
||||
name: 'metric1', |
||||
type: FieldType.number, |
||||
config: f1Config, |
||||
state: {}, |
||||
}); |
||||
|
||||
df2.addField({ |
||||
name: 'metric2', |
||||
type: FieldType.number, |
||||
config: f2Config, |
||||
state: {}, |
||||
}); |
||||
|
||||
return preparePlotFrame([df1, df2]); |
||||
} |
||||
|
||||
describe('GraphNG utils', () => { |
||||
describe('preparePlotConfigBuilder', () => { |
||||
const frame = mockDataFrame(); |
||||
|
||||
const config: BarChartOptions = { |
||||
orientation: VizOrientation.Auto, |
||||
groupWidth: 20, |
||||
barWidth: 2, |
||||
showValue: BarValueVisibility.Always, |
||||
legend: { |
||||
displayMode: LegendDisplayMode.List, |
||||
placement: 'bottom', |
||||
calcs: [], |
||||
}, |
||||
stacking: BarStackingMode.None, |
||||
}; |
||||
|
||||
it.each([VizOrientation.Auto, VizOrientation.Horizontal, VizOrientation.Vertical])('orientation', (v) => { |
||||
expect( |
||||
preparePlotConfigBuilder(frame!, { colors: { panelBg: '#000000' } } as GrafanaTheme, { |
||||
...config, |
||||
orientation: v, |
||||
}) |
||||
).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it.each([BarValueVisibility.Always, BarValueVisibility.Auto])('value visibility', (v) => { |
||||
expect( |
||||
preparePlotConfigBuilder(frame!, { colors: { panelBg: '#000000' } } as GrafanaTheme, { |
||||
...config, |
||||
showValue: v, |
||||
}) |
||||
).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it.each([BarStackingMode.None, BarStackingMode.Percent, BarStackingMode.Standard])('stacking', (v) => { |
||||
expect( |
||||
preparePlotConfigBuilder(frame!, { colors: { panelBg: '#000000' } } as GrafanaTheme, { |
||||
...config, |
||||
stacking: v, |
||||
}) |
||||
).toMatchSnapshot(); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,190 @@ |
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; |
||||
import { |
||||
DataFrame, |
||||
FieldType, |
||||
formattedValueToString, |
||||
getFieldColorModeForField, |
||||
getFieldDisplayName, |
||||
getFieldSeriesColor, |
||||
GrafanaTheme, |
||||
MutableDataFrame, |
||||
VizOrientation, |
||||
} from '@grafana/data'; |
||||
import { BarChartFieldConfig, BarChartOptions, BarValueVisibility, defaultBarChartFieldConfig } from './types'; |
||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '../uPlot/config'; |
||||
import { BarsOptions, getConfig } from './bars'; |
||||
import { FIXED_UNIT } from '../GraphNG/GraphNG'; |
||||
|
||||
/** @alpha */ |
||||
export function preparePlotConfigBuilder( |
||||
data: DataFrame, |
||||
theme: GrafanaTheme, |
||||
{ orientation, showValue, groupWidth, barWidth }: BarChartOptions |
||||
) { |
||||
const builder = new UPlotConfigBuilder(); |
||||
|
||||
// bar orientation -> x scale orientation & direction
|
||||
let xOri = ScaleOrientation.Vertical; |
||||
let xDir = ScaleDirection.Down; |
||||
let yOri = ScaleOrientation.Horizontal; |
||||
let yDir = ScaleDirection.Right; |
||||
|
||||
if (orientation === VizOrientation.Vertical) { |
||||
xOri = ScaleOrientation.Horizontal; |
||||
xDir = ScaleDirection.Right; |
||||
yOri = ScaleOrientation.Vertical; |
||||
yDir = ScaleDirection.Up; |
||||
} |
||||
|
||||
const formatValue = |
||||
showValue !== BarValueVisibility.Never |
||||
? (seriesIdx: number, value: any) => formattedValueToString(data.fields[seriesIdx].display!(value)) |
||||
: undefined; |
||||
|
||||
// Use bar width when only one field
|
||||
if (data.fields.length === 2) { |
||||
groupWidth = barWidth; |
||||
barWidth = 1; |
||||
} |
||||
|
||||
const opts: BarsOptions = { |
||||
xOri, |
||||
xDir, |
||||
groupWidth, |
||||
barWidth, |
||||
formatValue, |
||||
onHover: (seriesIdx: number, valueIdx: number) => { |
||||
console.log('hover', { seriesIdx, valueIdx }); |
||||
}, |
||||
onLeave: (seriesIdx: number, valueIdx: number) => { |
||||
console.log('leave', { seriesIdx, valueIdx }); |
||||
}, |
||||
}; |
||||
|
||||
const config = getConfig(opts); |
||||
|
||||
builder.addHook('init', config.init); |
||||
builder.addHook('drawClear', config.drawClear); |
||||
builder.addHook('setCursor', config.setCursor); |
||||
|
||||
builder.setCursor(config.cursor); |
||||
builder.setSelect(config.select); |
||||
|
||||
builder.addScale({ |
||||
scaleKey: 'x', |
||||
isTime: false, |
||||
distribution: ScaleDistribution.Ordinal, |
||||
orientation: xOri, |
||||
direction: xDir, |
||||
}); |
||||
|
||||
builder.addAxis({ |
||||
scaleKey: 'x', |
||||
isTime: false, |
||||
placement: xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left, |
||||
splits: config.xSplits, |
||||
values: config.xValues, |
||||
grid: false, |
||||
ticks: false, |
||||
gap: 15, |
||||
theme, |
||||
}); |
||||
|
||||
let seriesIndex = 0; |
||||
|
||||
// iterate the y values
|
||||
for (let i = 1; i < data.fields.length; i++) { |
||||
const field = data.fields[i]; |
||||
|
||||
field.state!.seriesIndex = seriesIndex++; |
||||
|
||||
const customConfig: BarChartFieldConfig = { ...defaultBarChartFieldConfig, ...field.config.custom }; |
||||
|
||||
const scaleKey = field.config.unit || FIXED_UNIT; |
||||
const colorMode = getFieldColorModeForField(field); |
||||
const scaleColor = getFieldSeriesColor(field, theme); |
||||
const seriesColor = scaleColor.color; |
||||
|
||||
builder.addSeries({ |
||||
scaleKey, |
||||
pxAlign: false, |
||||
lineWidth: customConfig.lineWidth, |
||||
lineColor: seriesColor, |
||||
//lineStyle: customConfig.lineStyle,
|
||||
fillOpacity: customConfig.fillOpacity, |
||||
theme, |
||||
colorMode, |
||||
pathBuilder: config.drawBars, |
||||
pointsBuilder: config.drawPoints, |
||||
show: !customConfig.hideFrom?.graph, |
||||
gradientMode: customConfig.gradientMode, |
||||
thresholds: field.config.thresholds, |
||||
|
||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||||
dataFrameFieldIndex: { |
||||
fieldIndex: i, |
||||
frameIndex: 0, |
||||
}, |
||||
fieldName: getFieldDisplayName(field, data), |
||||
hideInLegend: customConfig.hideFrom?.legend, |
||||
}); |
||||
|
||||
// The builder will manage unique scaleKeys and combine where appropriate
|
||||
builder.addScale({ |
||||
scaleKey, |
||||
min: field.config.min, |
||||
max: field.config.max, |
||||
softMin: customConfig.axisSoftMin, |
||||
softMax: customConfig.axisSoftMax, |
||||
orientation: yOri, |
||||
direction: yDir, |
||||
}); |
||||
|
||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) { |
||||
let placement = customConfig.axisPlacement; |
||||
if (!placement || placement === AxisPlacement.Auto) { |
||||
placement = AxisPlacement.Left; |
||||
} |
||||
if (xOri === 1) { |
||||
if (placement === AxisPlacement.Left) { |
||||
placement = AxisPlacement.Bottom; |
||||
} |
||||
if (placement === AxisPlacement.Right) { |
||||
placement = AxisPlacement.Top; |
||||
} |
||||
} |
||||
|
||||
builder.addAxis({ |
||||
scaleKey, |
||||
label: customConfig.axisLabel, |
||||
size: customConfig.axisWidth, |
||||
placement, |
||||
formatValue: (v) => formattedValueToString(field.display!(v)), |
||||
theme, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
return builder; |
||||
} |
||||
|
||||
/** @internal */ |
||||
export function preparePlotFrame(data: DataFrame[]) { |
||||
const firstFrame = data[0]; |
||||
const firstString = firstFrame.fields.find((f) => f.type === FieldType.string); |
||||
|
||||
if (!firstString) { |
||||
throw new Error('No string field in DF'); |
||||
} |
||||
|
||||
const resultFrame = new MutableDataFrame(); |
||||
resultFrame.addField(firstString); |
||||
|
||||
for (const f of firstFrame.fields) { |
||||
if (f.type === FieldType.number) { |
||||
resultFrame.addField(f); |
||||
} |
||||
} |
||||
|
||||
return resultFrame; |
||||
} |
||||
@ -0,0 +1,153 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` |
||||
UPlotConfigBuilder { |
||||
"axes": Object { |
||||
"__fixed": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"formatValue": [Function], |
||||
"label": undefined, |
||||
"placement": "left", |
||||
"scaleKey": "__fixed", |
||||
"size": undefined, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
"x": UPlotAxisBuilder { |
||||
"props": Object { |
||||
"isTime": true, |
||||
"placement": "bottom", |
||||
"scaleKey": "x", |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"timeZone": "browser", |
||||
}, |
||||
}, |
||||
}, |
||||
"bands": Array [], |
||||
"getTimeZone": [Function], |
||||
"hasBottomAxis": true, |
||||
"hasLeftAxis": true, |
||||
"hooks": Object {}, |
||||
"scales": Array [ |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"isTime": true, |
||||
"orientation": 0, |
||||
"range": [Function], |
||||
"scaleKey": "x", |
||||
}, |
||||
}, |
||||
UPlotScaleBuilder { |
||||
"props": Object { |
||||
"direction": 1, |
||||
"distribution": undefined, |
||||
"log": undefined, |
||||
"max": undefined, |
||||
"min": undefined, |
||||
"orientation": 1, |
||||
"scaleKey": "__fixed", |
||||
"softMax": undefined, |
||||
"softMin": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"series": Array [ |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"barAlignment": undefined, |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 0, |
||||
}, |
||||
"drawStyle": "line", |
||||
"fieldName": "Metric 1", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "opacity", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#ff0000", |
||||
"lineInterpolation": "linear", |
||||
"lineStyle": Object { |
||||
"dash": Array [ |
||||
1, |
||||
2, |
||||
], |
||||
"fill": "dash", |
||||
}, |
||||
"lineWidth": 2, |
||||
"pointColor": "#808080", |
||||
"pointSize": undefined, |
||||
"scaleKey": "__fixed", |
||||
"show": true, |
||||
"showPoints": "always", |
||||
"spanNulls": false, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
UPlotSeriesBuilder { |
||||
"props": Object { |
||||
"barAlignment": -1, |
||||
"colorMode": Object { |
||||
"description": "Derive colors from thresholds", |
||||
"getCalculator": [Function], |
||||
"id": "thresholds", |
||||
"isByValue": true, |
||||
"name": "From thresholds", |
||||
}, |
||||
"dataFrameFieldIndex": Object { |
||||
"fieldIndex": 1, |
||||
"frameIndex": 1, |
||||
}, |
||||
"drawStyle": "bars", |
||||
"fieldName": "Metric 2", |
||||
"fillOpacity": 0.1, |
||||
"gradientMode": "hue", |
||||
"hideInLegend": undefined, |
||||
"lineColor": "#ff0000", |
||||
"lineInterpolation": "linear", |
||||
"lineStyle": Object { |
||||
"dash": Array [ |
||||
1, |
||||
2, |
||||
], |
||||
"fill": "dash", |
||||
}, |
||||
"lineWidth": 2, |
||||
"pointColor": "#808080", |
||||
"pointSize": undefined, |
||||
"scaleKey": "__fixed", |
||||
"show": true, |
||||
"showPoints": "always", |
||||
"spanNulls": false, |
||||
"theme": Object { |
||||
"colors": Object { |
||||
"panelBg": "#000000", |
||||
}, |
||||
}, |
||||
"thresholds": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
"tzDate": [Function], |
||||
} |
||||
`; |
||||
@ -0,0 +1,45 @@ |
||||
import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data'; |
||||
import { XYFieldMatchers } from './types'; |
||||
import React, { useCallback, useContext } from 'react'; |
||||
|
||||
/** @alpha */ |
||||
interface GraphNGContextType { |
||||
mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex; |
||||
dimFields: XYFieldMatchers; |
||||
} |
||||
|
||||
/** @alpha */ |
||||
export const GraphNGContext = React.createContext<GraphNGContextType>({} as GraphNGContextType); |
||||
|
||||
/** |
||||
* @alpha |
||||
* Exposes API for data frame inspection in Plot plugins |
||||
*/ |
||||
export const useGraphNGContext = () => { |
||||
const graphCtx = useContext<GraphNGContextType>(GraphNGContext); |
||||
|
||||
const getXAxisField = useCallback( |
||||
(data: DataFrame[]) => { |
||||
const xFieldMatcher = graphCtx.dimFields.x; |
||||
let xField: Field | null = null; |
||||
|
||||
for (let i = 0; i < data.length; i++) { |
||||
const frame = data[i]; |
||||
for (let j = 0; j < frame.fields.length; j++) { |
||||
if (xFieldMatcher(frame.fields[j], frame, data)) { |
||||
xField = frame.fields[j]; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return xField; |
||||
}, |
||||
[graphCtx] |
||||
); |
||||
|
||||
return { |
||||
...graphCtx, |
||||
getXAxisField, |
||||
}; |
||||
}; |
||||
@ -0,0 +1,94 @@ |
||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; |
||||
import { |
||||
DefaultTimeZone, |
||||
FieldConfig, |
||||
FieldMatcherID, |
||||
fieldMatchers, |
||||
FieldType, |
||||
getDefaultTimeRange, |
||||
GrafanaTheme, |
||||
MutableDataFrame, |
||||
} from '@grafana/data'; |
||||
import { BarAlignment, DrawStyle, GraphFieldConfig, GraphGradientMode, LineInterpolation, PointVisibility } from '..'; |
||||
|
||||
function mockDataFrame() { |
||||
const df1 = new MutableDataFrame({ |
||||
refId: 'A', |
||||
fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 3] }], |
||||
}); |
||||
const df2 = new MutableDataFrame({ |
||||
refId: 'B', |
||||
fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }], |
||||
}); |
||||
|
||||
const f1Config: FieldConfig<GraphFieldConfig> = { |
||||
displayName: 'Metric 1', |
||||
decimals: 2, |
||||
custom: { |
||||
drawStyle: DrawStyle.Line, |
||||
gradientMode: GraphGradientMode.Opacity, |
||||
lineColor: '#ff0000', |
||||
lineWidth: 2, |
||||
lineInterpolation: LineInterpolation.Linear, |
||||
lineStyle: { |
||||
fill: 'dash', |
||||
dash: [1, 2], |
||||
}, |
||||
spanNulls: false, |
||||
fillColor: '#ff0000', |
||||
fillOpacity: 0.1, |
||||
showPoints: PointVisibility.Always, |
||||
}, |
||||
}; |
||||
|
||||
const f2Config: FieldConfig<GraphFieldConfig> = { |
||||
displayName: 'Metric 2', |
||||
decimals: 2, |
||||
custom: { |
||||
drawStyle: DrawStyle.Bars, |
||||
gradientMode: GraphGradientMode.Hue, |
||||
lineColor: '#ff0000', |
||||
lineWidth: 2, |
||||
lineInterpolation: LineInterpolation.Linear, |
||||
lineStyle: { |
||||
fill: 'dash', |
||||
dash: [1, 2], |
||||
}, |
||||
barAlignment: BarAlignment.Before, |
||||
fillColor: '#ff0000', |
||||
fillOpacity: 0.1, |
||||
showPoints: PointVisibility.Always, |
||||
}, |
||||
}; |
||||
|
||||
df1.addField({ |
||||
name: 'metric1', |
||||
type: FieldType.number, |
||||
config: f1Config, |
||||
}); |
||||
|
||||
df2.addField({ |
||||
name: 'metric2', |
||||
type: FieldType.number, |
||||
config: f2Config, |
||||
}); |
||||
|
||||
return preparePlotFrame([df1, df2], { |
||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), |
||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}), |
||||
}); |
||||
} |
||||
|
||||
describe('GraphNG utils', () => { |
||||
test('preparePlotConfigBuilder', () => { |
||||
const frame = mockDataFrame(); |
||||
expect( |
||||
preparePlotConfigBuilder( |
||||
frame!, |
||||
{ colors: { panelBg: '#000000' } } as GrafanaTheme, |
||||
getDefaultTimeRange, |
||||
() => DefaultTimeZone |
||||
) |
||||
).toMatchSnapshot(); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,198 @@ |
||||
import React from 'react'; |
||||
import isNumber from 'lodash/isNumber'; |
||||
import { GraphNGLegendEventMode, XYFieldMatchers } from './types'; |
||||
import { |
||||
DataFrame, |
||||
FieldConfig, |
||||
FieldType, |
||||
formattedValueToString, |
||||
getFieldColorModeForField, |
||||
getFieldDisplayName, |
||||
getFieldSeriesColor, |
||||
GrafanaTheme, |
||||
outerJoinDataFrames, |
||||
TimeRange, |
||||
TimeZone, |
||||
} from '@grafana/data'; |
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; |
||||
import { FIXED_UNIT } from './GraphNG'; |
||||
import { |
||||
AxisPlacement, |
||||
DrawStyle, |
||||
GraphFieldConfig, |
||||
PointVisibility, |
||||
ScaleDirection, |
||||
ScaleOrientation, |
||||
} from '../uPlot/config'; |
||||
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); |
||||
|
||||
const defaultConfig: GraphFieldConfig = { |
||||
drawStyle: DrawStyle.Line, |
||||
showPoints: PointVisibility.Auto, |
||||
axisPlacement: AxisPlacement.Auto, |
||||
}; |
||||
|
||||
export function mapMouseEventToMode(event: React.MouseEvent): GraphNGLegendEventMode { |
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) { |
||||
return GraphNGLegendEventMode.AppendToSelection; |
||||
} |
||||
return GraphNGLegendEventMode.ToggleSelection; |
||||
} |
||||
|
||||
export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers) { |
||||
return outerJoinDataFrames({ |
||||
frames: data, |
||||
joinBy: dimFields.x, |
||||
keep: dimFields.y, |
||||
keepOriginIndices: true, |
||||
}); |
||||
} |
||||
|
||||
export function preparePlotConfigBuilder( |
||||
frame: DataFrame, |
||||
theme: GrafanaTheme, |
||||
getTimeRange: () => TimeRange, |
||||
getTimeZone: () => TimeZone |
||||
): UPlotConfigBuilder { |
||||
const builder = new UPlotConfigBuilder(getTimeZone); |
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = frame.fields[0]; |
||||
let seriesIndex = 0; |
||||
|
||||
if (xField.type === FieldType.time) { |
||||
builder.addScale({ |
||||
scaleKey: 'x', |
||||
orientation: ScaleOrientation.Horizontal, |
||||
direction: ScaleDirection.Right, |
||||
isTime: true, |
||||
range: () => { |
||||
const r = getTimeRange(); |
||||
return [r.from.valueOf(), r.to.valueOf()]; |
||||
}, |
||||
}); |
||||
|
||||
builder.addAxis({ |
||||
scaleKey: 'x', |
||||
isTime: true, |
||||
placement: AxisPlacement.Bottom, |
||||
timeZone: getTimeZone(), |
||||
theme, |
||||
}); |
||||
} else { |
||||
// Not time!
|
||||
builder.addScale({ |
||||
scaleKey: 'x', |
||||
orientation: ScaleOrientation.Horizontal, |
||||
direction: ScaleDirection.Right, |
||||
}); |
||||
|
||||
builder.addAxis({ |
||||
scaleKey: 'x', |
||||
placement: AxisPlacement.Bottom, |
||||
theme, |
||||
}); |
||||
} |
||||
|
||||
let indexByName: Map<string, number> | undefined = undefined; |
||||
|
||||
for (let i = 0; i < frame.fields.length; i++) { |
||||
const field = frame.fields[i]; |
||||
const config = field.config as FieldConfig<GraphFieldConfig>; |
||||
const customConfig: GraphFieldConfig = { |
||||
...defaultConfig, |
||||
...config.custom, |
||||
}; |
||||
|
||||
if (field === xField || field.type !== FieldType.number) { |
||||
continue; |
||||
} |
||||
field.state!.seriesIndex = seriesIndex++; |
||||
|
||||
const fmt = field.display ?? defaultFormatter; |
||||
const scaleKey = config.unit || FIXED_UNIT; |
||||
const colorMode = getFieldColorModeForField(field); |
||||
const scaleColor = getFieldSeriesColor(field, theme); |
||||
const seriesColor = scaleColor.color; |
||||
|
||||
// The builder will manage unique scaleKeys and combine where appropriate
|
||||
builder.addScale({ |
||||
scaleKey, |
||||
orientation: ScaleOrientation.Vertical, |
||||
direction: ScaleDirection.Up, |
||||
distribution: customConfig.scaleDistribution?.type, |
||||
log: customConfig.scaleDistribution?.log, |
||||
min: field.config.min, |
||||
max: field.config.max, |
||||
softMin: customConfig.axisSoftMin, |
||||
softMax: customConfig.axisSoftMax, |
||||
}); |
||||
|
||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) { |
||||
builder.addAxis({ |
||||
scaleKey, |
||||
label: customConfig.axisLabel, |
||||
size: customConfig.axisWidth, |
||||
placement: customConfig.axisPlacement ?? AxisPlacement.Auto, |
||||
formatValue: (v) => formattedValueToString(fmt(v)), |
||||
theme, |
||||
}); |
||||
} |
||||
|
||||
const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints; |
||||
|
||||
let { fillOpacity } = customConfig; |
||||
if (customConfig.fillBelowTo) { |
||||
if (!indexByName) { |
||||
indexByName = getNamesToFieldIndex(frame); |
||||
} |
||||
const t = indexByName.get(getFieldDisplayName(field, frame)); |
||||
const b = indexByName.get(customConfig.fillBelowTo); |
||||
if (isNumber(b) && isNumber(t)) { |
||||
builder.addBand({ |
||||
series: [t, b], |
||||
fill: null as any, // using null will have the band use fill options from `t`
|
||||
}); |
||||
} |
||||
if (!fillOpacity) { |
||||
fillOpacity = 35; // default from flot
|
||||
} |
||||
} |
||||
|
||||
builder.addSeries({ |
||||
scaleKey, |
||||
showPoints, |
||||
colorMode, |
||||
fillOpacity, |
||||
theme, |
||||
drawStyle: customConfig.drawStyle!, |
||||
lineColor: customConfig.lineColor ?? seriesColor, |
||||
lineWidth: customConfig.lineWidth, |
||||
lineInterpolation: customConfig.lineInterpolation, |
||||
lineStyle: customConfig.lineStyle, |
||||
barAlignment: customConfig.barAlignment, |
||||
pointSize: customConfig.pointSize, |
||||
pointColor: customConfig.pointColor ?? seriesColor, |
||||
spanNulls: customConfig.spanNulls || false, |
||||
show: !customConfig.hideFrom?.graph, |
||||
gradientMode: customConfig.gradientMode, |
||||
thresholds: config.thresholds, |
||||
|
||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||||
dataFrameFieldIndex: field.state?.origin, |
||||
fieldName: getFieldDisplayName(field, frame), |
||||
hideInLegend: customConfig.hideFrom?.legend, |
||||
}); |
||||
} |
||||
|
||||
return builder; |
||||
} |
||||
|
||||
export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> { |
||||
const names = new Map<string, number>(); |
||||
for (let i = 0; i < frame.fields.length; i++) { |
||||
names.set(getFieldDisplayName(frame.fields[i], frame), i); |
||||
} |
||||
return names; |
||||
} |
||||
@ -0,0 +1,25 @@ |
||||
import { DataFrame, FieldConfig, FieldSparkline, IndexVector } from '@grafana/data'; |
||||
import { GraphFieldConfig } from '../uPlot/config'; |
||||
|
||||
/** @internal |
||||
* Given a sparkline config returns a DataFrame ready to be turned into Plot data set |
||||
**/ |
||||
export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig<GraphFieldConfig>): DataFrame { |
||||
const length = sparkline.y.values.length; |
||||
const yFieldConfig = { |
||||
...sparkline.y.config, |
||||
...config, |
||||
}; |
||||
|
||||
return { |
||||
refId: 'sparkline', |
||||
fields: [ |
||||
sparkline.x ?? IndexVector.newField(length), |
||||
{ |
||||
...sparkline.y, |
||||
config: yFieldConfig, |
||||
}, |
||||
], |
||||
length, |
||||
}; |
||||
} |
||||
@ -0,0 +1,97 @@ |
||||
import React, { useCallback } from 'react'; |
||||
import { DataFrame, DisplayValue, fieldReducers, reduceField } from '@grafana/data'; |
||||
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; |
||||
import { VizLegendItem, VizLegendOptions } from '../VizLegend/types'; |
||||
import { AxisPlacement } from './config'; |
||||
import { VizLayout } from '../VizLayout/VizLayout'; |
||||
import { mapMouseEventToMode } from '../GraphNG/utils'; |
||||
import { VizLegend } from '../VizLegend/VizLegend'; |
||||
import { GraphNGLegendEvent } from '..'; |
||||
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); |
||||
|
||||
interface PlotLegendProps extends VizLegendOptions { |
||||
data: DataFrame[]; |
||||
config: UPlotConfigBuilder; |
||||
onSeriesColorChange?: (label: string, color: string) => void; |
||||
onLegendClick?: (event: GraphNGLegendEvent) => void; |
||||
} |
||||
|
||||
export const PlotLegend: React.FC<PlotLegendProps> = ({ |
||||
data, |
||||
config, |
||||
onSeriesColorChange, |
||||
onLegendClick, |
||||
...legend |
||||
}) => { |
||||
const onLegendLabelClick = useCallback( |
||||
(legend: VizLegendItem, event: React.MouseEvent) => { |
||||
const { fieldIndex } = legend; |
||||
|
||||
if (!onLegendClick || !fieldIndex) { |
||||
return; |
||||
} |
||||
|
||||
onLegendClick({ |
||||
fieldIndex, |
||||
mode: mapMouseEventToMode(event), |
||||
}); |
||||
}, |
||||
[onLegendClick] |
||||
); |
||||
|
||||
const legendItems = config |
||||
.getSeries() |
||||
.map<VizLegendItem | undefined>((s) => { |
||||
const seriesConfig = s.props; |
||||
const fieldIndex = seriesConfig.dataFrameFieldIndex; |
||||
const axisPlacement = config.getAxisPlacement(s.props.scaleKey); |
||||
|
||||
if (seriesConfig.hideInLegend || !fieldIndex) { |
||||
return undefined; |
||||
} |
||||
|
||||
const field = data[fieldIndex.frameIndex]?.fields[fieldIndex.fieldIndex]; |
||||
|
||||
return { |
||||
disabled: !seriesConfig.show ?? false, |
||||
fieldIndex, |
||||
color: seriesConfig.lineColor!, |
||||
label: seriesConfig.fieldName, |
||||
yAxis: axisPlacement === AxisPlacement.Left ? 1 : 2, |
||||
getDisplayValues: () => { |
||||
if (!legend.calcs?.length) { |
||||
return []; |
||||
} |
||||
|
||||
const fmt = field.display ?? defaultFormatter; |
||||
const fieldCalcs = reduceField({ |
||||
field, |
||||
reducers: legend.calcs, |
||||
}); |
||||
|
||||
return legend.calcs.map<DisplayValue>((reducer) => { |
||||
return { |
||||
...fmt(fieldCalcs[reducer]), |
||||
title: fieldReducers.get(reducer).name, |
||||
}; |
||||
}); |
||||
}, |
||||
}; |
||||
}) |
||||
.filter((i) => i !== undefined) as VizLegendItem[]; |
||||
|
||||
return ( |
||||
<VizLayout.Legend placement={legend.placement} maxHeight="35%" maxWidth="60%"> |
||||
<VizLegend |
||||
onLabelClick={onLegendLabelClick} |
||||
placement={legend.placement} |
||||
items={legendItems} |
||||
displayMode={legend.displayMode} |
||||
onSeriesColorChange={onSeriesColorChange} |
||||
/> |
||||
</VizLayout.Legend> |
||||
); |
||||
}; |
||||
|
||||
PlotLegend.displayName = 'PlotLegend'; |
||||
Loading…
Reference in new issue