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 React from 'react'; |
||||||
import { |
import { AlignedData } from 'uplot'; |
||||||
compareDataFrameStructures, |
import { compareArrayValues, compareDataFrameStructures, DataFrame, TimeRange } from '@grafana/data'; |
||||||
DataFrame, |
|
||||||
DefaultTimeZone, |
|
||||||
formattedValueToString, |
|
||||||
getFieldDisplayName, |
|
||||||
getFieldSeriesColor, |
|
||||||
getFieldColorModeForField, |
|
||||||
TimeRange, |
|
||||||
VizOrientation, |
|
||||||
fieldReducers, |
|
||||||
reduceField, |
|
||||||
DisplayValue, |
|
||||||
} from '@grafana/data'; |
|
||||||
|
|
||||||
import { VizLayout } from '../VizLayout/VizLayout'; |
import { VizLayout } from '../VizLayout/VizLayout'; |
||||||
import { Themeable } from '../../types'; |
import { Themeable } from '../../types'; |
||||||
import { useRevision } from '../uPlot/hooks'; |
|
||||||
import { UPlotChart } from '../uPlot/Plot'; |
import { UPlotChart } from '../uPlot/Plot'; |
||||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; |
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; |
||||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '../uPlot/config'; |
import { GraphNGLegendEvent } from '../GraphNG/types'; |
||||||
import { useTheme } from '../../themes'; |
import { BarChartOptions } from './types'; |
||||||
import { GraphNGLegendEvent, GraphNGLegendEventMode } from '../GraphNG/types'; |
import { withTheme } from '../../themes'; |
||||||
import { FIXED_UNIT } from '../GraphNG/GraphNG'; |
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; |
||||||
import { LegendDisplayMode, VizLegendItem } from '../VizLegend/types'; |
import { preparePlotData } from '../uPlot/utils'; |
||||||
import { VizLegend } from '../VizLegend/VizLegend'; |
import { LegendDisplayMode } from '../VizLegend/types'; |
||||||
|
import { PlotLegend } from '../uPlot/PlotLegend'; |
||||||
import { BarChartFieldConfig, BarChartOptions, BarValueVisibility, defaultBarChartFieldConfig } from './types'; |
|
||||||
import { BarsOptions, getConfig } from './bars'; |
|
||||||
|
|
||||||
/** |
/** |
||||||
* @alpha |
* @alpha |
||||||
*/ |
*/ |
||||||
export interface Props extends Themeable, BarChartOptions { |
export interface BarChartProps extends Themeable, BarChartOptions { |
||||||
height: number; |
height: number; |
||||||
width: number; |
width: number; |
||||||
data: DataFrame; |
data: DataFrame[]; |
||||||
onLegendClick?: (event: GraphNGLegendEvent) => void; |
onLegendClick?: (event: GraphNGLegendEvent) => void; |
||||||
onSeriesColorChange?: (label: string, color: string) => void; |
onSeriesColorChange?: (label: string, color: string) => void; |
||||||
} |
} |
||||||
|
|
||||||
/** |
interface BarChartState { |
||||||
* @alpha |
data: AlignedData; |
||||||
*/ |
alignedDataFrame: DataFrame; |
||||||
export const BarChart: React.FunctionComponent<Props> = ({ |
config?: UPlotConfigBuilder; |
||||||
width, |
|
||||||
height, |
|
||||||
data, |
|
||||||
orientation, |
|
||||||
groupWidth, |
|
||||||
barWidth, |
|
||||||
showValue, |
|
||||||
legend, |
|
||||||
onLegendClick, |
|
||||||
onSeriesColorChange, |
|
||||||
...plotProps |
|
||||||
}) => { |
|
||||||
if (!data || data.fields.length < 2) { |
|
||||||
return <div>Missing data</div>; |
|
||||||
} |
} |
||||||
|
|
||||||
// dominik? TODO? can this all be moved into `useRevision`
|
class UnthemedBarChart extends React.Component<BarChartProps, BarChartState> { |
||||||
const compareFrames = useCallback((a?: DataFrame | null, b?: DataFrame | null) => { |
constructor(props: BarChartProps) { |
||||||
if (a && b) { |
super(props); |
||||||
return compareDataFrameStructures(a, b); |
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
|
if (!frame) { |
||||||
const configBuilder = useMemo(() => { |
return { ...state }; |
||||||
if (!orientation || orientation === VizOrientation.Auto) { |
|
||||||
orientation = width < height ? VizOrientation.Horizontal : VizOrientation.Vertical; |
|
||||||
} |
} |
||||||
|
|
||||||
// bar orientation -> x scale orientation & direction
|
return { |
||||||
let xOri: ScaleOrientation, xDir: ScaleDirection, yOri: ScaleOrientation, yDir: ScaleDirection; |
...state, |
||||||
|
data: preparePlotData(frame), |
||||||
if (orientation === VizOrientation.Vertical) { |
alignedDataFrame: frame, |
||||||
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; |
|
||||||
} |
} |
||||||
|
|
||||||
const formatValue = |
componentDidMount() { |
||||||
showValue !== BarValueVisibility.Never |
const { alignedDataFrame } = this.state; |
||||||
? (seriesIdx: number, value: any) => formattedValueToString(data.fields[seriesIdx].display!(value)) |
|
||||||
: undefined; |
|
||||||
|
|
||||||
// Use bar width when only one field
|
if (!alignedDataFrame) { |
||||||
if (data.fields.length === 2) { |
return; |
||||||
groupWidth = barWidth; |
|
||||||
barWidth = 1; |
|
||||||
} |
} |
||||||
|
|
||||||
const opts: BarsOptions = { |
this.setState({ |
||||||
xOri, |
config: preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props), |
||||||
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, |
|
||||||
}); |
}); |
||||||
|
} |
||||||
|
|
||||||
builder.addAxis({ |
componentDidUpdate(prevProps: BarChartProps) { |
||||||
scaleKey: 'x', |
const { data, orientation, groupWidth, barWidth, showValue } = this.props; |
||||||
isTime: false, |
const { alignedDataFrame } = this.state; |
||||||
placement: xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left, |
let shouldConfigUpdate = false; |
||||||
splits: config.xSplits, |
let hasStructureChanged = false; |
||||||
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
|
if ( |
||||||
builder.addScale({ |
this.state.config === undefined || |
||||||
scaleKey, |
orientation !== prevProps.orientation || |
||||||
min: field.config.min, |
groupWidth !== prevProps.groupWidth || |
||||||
max: field.config.max, |
barWidth !== prevProps.barWidth || |
||||||
softMin: customConfig.axisSoftMin, |
showValue !== prevProps.showValue |
||||||
softMax: customConfig.axisSoftMax, |
) { |
||||||
orientation: yOri, |
shouldConfigUpdate = true; |
||||||
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; |
if (data !== prevProps.data) { |
||||||
|
if (!alignedDataFrame) { |
||||||
|
return; |
||||||
} |
} |
||||||
|
hasStructureChanged = !compareArrayValues(data, prevProps.data, compareDataFrameStructures); |
||||||
} |
} |
||||||
|
|
||||||
builder.addAxis({ |
if (shouldConfigUpdate || hasStructureChanged) { |
||||||
scaleKey, |
this.setState({ |
||||||
label: customConfig.axisLabel, |
config: preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props), |
||||||
size: customConfig.axisWidth, |
|
||||||
placement, |
|
||||||
formatValue: (v) => formattedValueToString(field.display!(v)), |
|
||||||
theme, |
|
||||||
}); |
}); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
return builder; |
renderLegend() { |
||||||
}, [data, configRev, orientation, width, height]); |
const { legend, onSeriesColorChange, onLegendClick, data } = this.props; |
||||||
|
const { config } = this.state; |
||||||
const onLabelClick = useCallback( |
|
||||||
(legend: VizLegendItem, event: React.MouseEvent) => { |
|
||||||
const { fieldIndex } = legend; |
|
||||||
|
|
||||||
if (!onLegendClick || !fieldIndex) { |
if (!config || legend.displayMode === LegendDisplayMode.Hidden) { |
||||||
return; |
return; |
||||||
} |
} |
||||||
|
return ( |
||||||
onLegendClick({ |
<PlotLegend |
||||||
fieldIndex, |
data={data} |
||||||
mode: GraphNGLegendEventMode.AppendToSelection, |
config={config} |
||||||
}); |
onSeriesColorChange={onSeriesColorChange} |
||||||
}, |
onLegendClick={onLegendClick} |
||||||
[onLegendClick, data] |
{...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]; |
render() { |
||||||
if (!field) { |
const { width, height } = this.props; |
||||||
return undefined; |
const { config, data } = this.state; |
||||||
} |
|
||||||
|
|
||||||
return { |
if (!config) { |
||||||
disabled: !seriesConfig.show ?? false, |
return null; |
||||||
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> |
|
||||||
); |
|
||||||
} |
} |
||||||
|
|
||||||
return ( |
return ( |
||||||
<VizLayout width={width} height={height} legend={legendElement}> |
<VizLayout width={width} height={height} legend={this.renderLegend()}> |
||||||
{(vizWidth: number, vizHeight: number) => ( |
{(vizWidth: number, vizHeight: number) => ( |
||||||
<UPlotChart |
<UPlotChart |
||||||
data={data} |
data={data} |
||||||
config={configBuilder} |
config={config} |
||||||
width={vizWidth} |
width={vizWidth} |
||||||
height={vizHeight} |
height={vizHeight} |
||||||
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
||||||
timeZone={DefaultTimeZone} |
|
||||||
/> |
/> |
||||||
)} |
)} |
||||||
</VizLayout> |
</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