BarChart: add alpha bar chart panel (#30323)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { DataFrame } from './dataFrame';
|
||||
|
||||
/**
|
||||
* Base class for editor builders
|
||||
*
|
||||
@@ -49,5 +51,5 @@ export interface OptionEditorConfig<TOptions, TSettings = any, TValue = any> {
|
||||
/**
|
||||
* Function that enables configuration of when option editor should be shown based on current panel option properties.
|
||||
*/
|
||||
showIf?: (currentOptions: TOptions) => boolean | undefined;
|
||||
showIf?: (currentOptions: TOptions, data?: DataFrame[]) => boolean | undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { toDataFrame, FieldType, VizOrientation } from '@grafana/data';
|
||||
import React from 'react';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { BarChart } from './BarChart';
|
||||
import { LegendDisplayMode } from '../VizLegend/types';
|
||||
import { prepDataForStorybook } from '../../utils/storybook/data';
|
||||
import { useTheme } from '../../themes';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { BarChartOptions, BarStackingMode, BarValueVisibility } from './types';
|
||||
|
||||
export default {
|
||||
title: 'Visualizations/BarChart',
|
||||
component: BarChart,
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
docs: {},
|
||||
},
|
||||
};
|
||||
|
||||
const getKnobs = () => {
|
||||
return {
|
||||
legendPlacement: select(
|
||||
'Legend placement',
|
||||
{
|
||||
bottom: 'bottom',
|
||||
right: 'right',
|
||||
},
|
||||
'bottom'
|
||||
),
|
||||
orientation: select(
|
||||
'Bar orientation',
|
||||
{
|
||||
vertical: VizOrientation.Vertical,
|
||||
horizontal: VizOrientation.Horizontal,
|
||||
},
|
||||
VizOrientation.Vertical
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const Basic: React.FC = () => {
|
||||
const { legendPlacement, orientation } = getKnobs();
|
||||
|
||||
const theme = useTheme();
|
||||
const frame = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'x', type: FieldType.string, values: ['group 1', 'group 2'] },
|
||||
{ name: 'a', type: FieldType.number, values: [10, 20] },
|
||||
{ name: 'b', type: FieldType.number, values: [30, 10] },
|
||||
],
|
||||
});
|
||||
|
||||
const data = prepDataForStorybook([frame], theme);
|
||||
|
||||
const options: BarChartOptions = {
|
||||
orientation: orientation,
|
||||
legend: { displayMode: LegendDisplayMode.List, placement: legendPlacement, calcs: [] },
|
||||
stacking: BarStackingMode.None,
|
||||
showValue: BarValueVisibility.Always,
|
||||
barWidth: 0.97,
|
||||
groupWidth: 0.7,
|
||||
};
|
||||
|
||||
return <BarChart data={data[0]} width={600} height={400} theme={theme} {...options} />;
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
compareDataFrameStructures,
|
||||
DataFrame,
|
||||
DefaultTimeZone,
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
getFieldSeriesColor,
|
||||
getFieldColorModeForField,
|
||||
TimeRange,
|
||||
VizOrientation,
|
||||
fieldReducers,
|
||||
reduceField,
|
||||
DisplayValue,
|
||||
} 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';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface Props extends Themeable, BarChartOptions {
|
||||
height: number;
|
||||
width: number;
|
||||
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>;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const configRev = useRevision(data, compareFrames);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
// Updates only when the structure changes
|
||||
const configBuilder = useMemo(() => {
|
||||
if (!orientation || orientation === VizOrientation.Auto) {
|
||||
orientation = width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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({
|
||||
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,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
lineColor: seriesColor,
|
||||
fillOpacity: customConfig.fillOpacity,
|
||||
theme,
|
||||
colorMode,
|
||||
pathBuilder: config.drawBars,
|
||||
pointsBuilder: config.drawPoints,
|
||||
show: !customConfig.hideFrom?.graph,
|
||||
gradientMode: customConfig.gradientMode,
|
||||
thresholds: field.config.thresholds,
|
||||
|
||||
/*
|
||||
lineColor: customConfig.lineColor ?? seriesColor,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
lineStyle: customConfig.lineStyle,
|
||||
*/
|
||||
|
||||
// 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;
|
||||
}, [data, configRev, orientation, width, height]);
|
||||
|
||||
const onLabelClick = useCallback(
|
||||
(legend: VizLegendItem, event: React.MouseEvent) => {
|
||||
const { fieldIndex } = legend;
|
||||
|
||||
if (!onLegendClick || !fieldIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
onLegendClick({
|
||||
fieldIndex,
|
||||
mode: GraphNGLegendEventMode.AppendToSelection,
|
||||
});
|
||||
},
|
||||
[onLegendClick, data]
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VizLayout width={width} height={height} legend={legendElement}>
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart
|
||||
data={data}
|
||||
config={configBuilder}
|
||||
width={vizWidth}
|
||||
height={vizHeight}
|
||||
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
||||
timeZone={DefaultTimeZone}
|
||||
/>
|
||||
)}
|
||||
</VizLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,287 @@
|
||||
import uPlot, { Axis, Series, Cursor, BBox } from 'uplot';
|
||||
import { Quadtree, Rect, pointWithin } from './quadtree';
|
||||
import { distribute, SPACE_BETWEEN } from './distribute';
|
||||
|
||||
const pxRatio = devicePixelRatio;
|
||||
|
||||
const groupDistr = SPACE_BETWEEN;
|
||||
const barDistr = SPACE_BETWEEN;
|
||||
|
||||
const font = Math.round(10 * pxRatio) + 'px Arial';
|
||||
|
||||
type WalkTwoCb = null | ((idx: number, offPx: number, dimPx: number) => void);
|
||||
|
||||
function walkTwo(
|
||||
groupWidth: number,
|
||||
barWidth: number,
|
||||
yIdx: number,
|
||||
xCount: number,
|
||||
yCount: number,
|
||||
xDim: number,
|
||||
xDraw?: WalkTwoCb,
|
||||
yDraw?: WalkTwoCb
|
||||
) {
|
||||
distribute(xCount, groupWidth, groupDistr, null, (ix, offPct, dimPct) => {
|
||||
let groupOffPx = xDim * offPct;
|
||||
let groupWidPx = xDim * dimPct;
|
||||
|
||||
xDraw && xDraw(ix, groupOffPx, groupWidPx);
|
||||
|
||||
yDraw &&
|
||||
distribute(yCount, barWidth, barDistr, yIdx, (iy, offPct, dimPct) => {
|
||||
let barOffPx = groupWidPx * offPct;
|
||||
let barWidPx = groupWidPx * dimPct;
|
||||
|
||||
yDraw(ix, groupOffPx + barOffPx, barWidPx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface BarsOptions {
|
||||
xOri: 1 | 0;
|
||||
xDir: 1 | -1;
|
||||
groupWidth: number;
|
||||
barWidth: number;
|
||||
formatValue?: (seriesIdx: number, value: any) => string;
|
||||
onHover?: (seriesIdx: number, valueIdx: any) => void;
|
||||
onLeave?: (seriesIdx: number, valueIdx: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getConfig(opts: BarsOptions) {
|
||||
const { xOri: ori, xDir: dir, groupWidth, barWidth, formatValue, onHover, onLeave } = opts;
|
||||
|
||||
let qt: Quadtree;
|
||||
|
||||
const drawBars: Series.PathBuilder = (u, sidx, i0, i1) => {
|
||||
return uPlot.orient(
|
||||
u,
|
||||
sidx,
|
||||
(series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect) => {
|
||||
const fill = new Path2D();
|
||||
|
||||
let numGroups = dataX.length;
|
||||
let barsPerGroup = u.series.length - 1;
|
||||
|
||||
let y0Pos = valToPosY(0, scaleY, yDim, yOff);
|
||||
|
||||
const _dir = dir * (ori === 0 ? 1 : -1);
|
||||
|
||||
walkTwo(groupWidth, barWidth, sidx - 1, numGroups, barsPerGroup, xDim, null, (ix, x0, wid) => {
|
||||
let lft = Math.round(xOff + (_dir === 1 ? x0 : xDim - x0 - wid));
|
||||
let barWid = Math.round(wid);
|
||||
|
||||
if (dataY[ix] != null) {
|
||||
let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff);
|
||||
|
||||
let btm = Math.round(Math.max(yPos, y0Pos));
|
||||
let top = Math.round(Math.min(yPos, y0Pos));
|
||||
let barHgt = btm - top;
|
||||
|
||||
rect(fill, lft, top, barWid, barHgt);
|
||||
|
||||
let x = ori === 0 ? Math.round(lft - xOff) : Math.round(top - yOff);
|
||||
let y = ori === 0 ? Math.round(top - yOff) : Math.round(lft - xOff);
|
||||
let w = ori === 0 ? barWid : barHgt;
|
||||
let h = ori === 0 ? barHgt : barWid;
|
||||
|
||||
qt.add({ x, y, w, h, sidx: sidx, didx: ix });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
stroke: fill,
|
||||
fill,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const drawPoints: Series.Points.Show =
|
||||
formatValue == null
|
||||
? false
|
||||
: (u, sidx, i0, i1) => {
|
||||
u.ctx.font = font;
|
||||
u.ctx.fillStyle = 'white';
|
||||
|
||||
uPlot.orient(
|
||||
u,
|
||||
sidx,
|
||||
(
|
||||
series,
|
||||
dataX,
|
||||
dataY,
|
||||
scaleX,
|
||||
scaleY,
|
||||
valToPosX,
|
||||
valToPosY,
|
||||
xOff,
|
||||
yOff,
|
||||
xDim,
|
||||
yDim,
|
||||
moveTo,
|
||||
lineTo,
|
||||
rect
|
||||
) => {
|
||||
let numGroups = dataX.length;
|
||||
let barsPerGroup = u.series.length - 1;
|
||||
|
||||
const _dir = dir * (ori === 0 ? 1 : -1);
|
||||
|
||||
walkTwo(groupWidth, barWidth, sidx - 1, numGroups, barsPerGroup, xDim, null, (ix, x0, wid) => {
|
||||
let lft = Math.round(xOff + (_dir === 1 ? x0 : xDim - x0 - wid));
|
||||
let barWid = Math.round(wid);
|
||||
|
||||
// prettier-ignore
|
||||
if (dataY[ix] != null) {
|
||||
let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff);
|
||||
|
||||
/* eslint-disable no-multi-spaces */
|
||||
let x = ori === 0 ? Math.round(lft + barWid / 2) : Math.round(yPos);
|
||||
let y = ori === 0 ? Math.round(yPos) : Math.round(lft + barWid / 2);
|
||||
|
||||
u.ctx.textAlign = ori === 0 ? 'center' : dataY[ix]! >= 0 ? 'left' : 'right';
|
||||
u.ctx.textBaseline = ori === 1 ? 'middle' : dataY[ix]! >= 0 ? 'bottom' : 'top';
|
||||
/* eslint-enable */
|
||||
|
||||
u.ctx.fillText(formatValue(sidx, dataY[ix]), x, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/*
|
||||
const yRange: Scale.Range = (u, dataMin, dataMax) => {
|
||||
// @ts-ignore
|
||||
let [min, max] = uPlot.rangeNum(0, dataMax, 0.05, true);
|
||||
return [0, max];
|
||||
};
|
||||
*/
|
||||
|
||||
const xSplits: Axis.Splits = (u: uPlot, axisIdx: number) => {
|
||||
const dim = ori === 0 ? u.bbox.width : u.bbox.height;
|
||||
const _dir = dir * (ori === 0 ? 1 : -1);
|
||||
|
||||
let splits: number[] = [];
|
||||
|
||||
distribute(u.data[0].length, groupWidth, groupDistr, null, (di, lftPct, widPct) => {
|
||||
let groupLftPx = (dim * lftPct) / pxRatio;
|
||||
let groupWidPx = (dim * widPct) / pxRatio;
|
||||
|
||||
let groupCenterPx = groupLftPx + groupWidPx / 2;
|
||||
|
||||
splits.push(u.posToVal(groupCenterPx, 'x'));
|
||||
});
|
||||
|
||||
return _dir === 1 ? splits : splits.reverse();
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const xValues: Axis.Values = (u) => u.data[0];
|
||||
|
||||
let hovered: Rect | null = null;
|
||||
|
||||
let barMark = document.createElement('div');
|
||||
barMark.classList.add('bar-mark');
|
||||
barMark.style.position = 'absolute';
|
||||
barMark.style.background = 'rgba(255,255,255,0.4)';
|
||||
|
||||
// hide crosshair cursor & hover points
|
||||
const cursor: Cursor = {
|
||||
x: false,
|
||||
y: false,
|
||||
points: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
|
||||
// disable selection
|
||||
// uPlot types do not export the Select interface prior to 1.6.4
|
||||
const select: Partial<BBox> = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
const init = (u: uPlot) => {
|
||||
let over = u.root.querySelector('.u-over')! as HTMLElement;
|
||||
over.style.overflow = 'hidden';
|
||||
over.appendChild(barMark);
|
||||
};
|
||||
|
||||
const drawClear = (u: uPlot) => {
|
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
|
||||
qt.clear();
|
||||
|
||||
// clear the path cache to force drawBars() to rebuild new quadtree
|
||||
u.series.forEach((s) => {
|
||||
// @ts-ignore
|
||||
s._paths = null;
|
||||
});
|
||||
};
|
||||
|
||||
// handle hover interaction with quadtree probing
|
||||
const setCursor = (u: uPlot) => {
|
||||
let found: Rect | null = null;
|
||||
let cx = u.cursor.left! * pxRatio;
|
||||
let cy = u.cursor.top! * pxRatio;
|
||||
|
||||
qt.get(cx, cy, 1, 1, (o) => {
|
||||
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
||||
found = o;
|
||||
}
|
||||
});
|
||||
|
||||
if (found) {
|
||||
// prettier-ignore
|
||||
if (found !== hovered) {
|
||||
/* eslint-disable no-multi-spaces */
|
||||
barMark.style.display = '';
|
||||
barMark.style.left = found!.x / pxRatio + 'px';
|
||||
barMark.style.top = found!.y / pxRatio + 'px';
|
||||
barMark.style.width = found!.w / pxRatio + 'px';
|
||||
barMark.style.height = found!.h / pxRatio + 'px';
|
||||
hovered = found;
|
||||
/* eslint-enable */
|
||||
|
||||
if (onHover != null) {
|
||||
onHover(hovered!.sidx, hovered!.didx);
|
||||
}
|
||||
}
|
||||
} else if (hovered != null) {
|
||||
if (onLeave != null) {
|
||||
onLeave(hovered!.sidx, hovered!.didx);
|
||||
}
|
||||
|
||||
hovered = null;
|
||||
barMark.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// cursor & select opts
|
||||
cursor,
|
||||
select,
|
||||
|
||||
// scale & axis opts
|
||||
// yRange,
|
||||
xValues,
|
||||
xSplits,
|
||||
|
||||
// pathbuilders
|
||||
drawBars,
|
||||
drawPoints,
|
||||
|
||||
// hooks
|
||||
init,
|
||||
drawClear,
|
||||
setCursor,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
function roundDec(val: number, dec: number) {
|
||||
return Math.round(val * (dec = 10 ** dec)) / dec;
|
||||
}
|
||||
|
||||
export const SPACE_BETWEEN = 1;
|
||||
export const SPACE_AROUND = 2;
|
||||
export const SPACE_EVENLY = 3;
|
||||
|
||||
const coord = (i: number, offs: number, iwid: number, gap: number) => roundDec(offs + i * (iwid + gap), 6);
|
||||
|
||||
export type Each = (idx: number, offPct: number, dimPct: number) => void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function distribute(numItems: number, sizeFactor: number, justify: number, onlyIdx: number | null, each: Each) {
|
||||
let space = 1 - sizeFactor;
|
||||
|
||||
/* eslint-disable no-multi-spaces */
|
||||
// prettier-ignore
|
||||
let gap = (
|
||||
justify === SPACE_BETWEEN ? space / (numItems - 1) :
|
||||
justify === SPACE_AROUND ? space / (numItems ) :
|
||||
justify === SPACE_EVENLY ? space / (numItems + 1) : 0
|
||||
);
|
||||
|
||||
if (isNaN(gap) || gap === Infinity) {
|
||||
gap = 0;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
let offs = (
|
||||
justify === SPACE_BETWEEN ? 0 :
|
||||
justify === SPACE_AROUND ? gap / 2 :
|
||||
justify === SPACE_EVENLY ? gap : 0
|
||||
);
|
||||
/* eslint-enable */
|
||||
|
||||
let iwid = sizeFactor / numItems;
|
||||
let _iwid = roundDec(iwid, 6);
|
||||
|
||||
if (onlyIdx == null) {
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
each(i, coord(i, offs, iwid, gap), _iwid);
|
||||
}
|
||||
} else {
|
||||
each(onlyIdx, coord(onlyIdx, offs, iwid, gap), _iwid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
const MAX_OBJECTS = 10;
|
||||
const MAX_LEVELS = 4;
|
||||
|
||||
export type Quads = [Quadtree, Quadtree, Quadtree, Quadtree];
|
||||
export type Rect = { x: number; y: number; w: number; h: number; [_: string]: any };
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function pointWithin(px: number, py: number, rlft: number, rtop: number, rrgt: number, rbtm: number) {
|
||||
return px >= rlft && px <= rrgt && py >= rtop && py <= rbtm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class Quadtree {
|
||||
o: Rect[];
|
||||
q: Quads | null;
|
||||
constructor(public x: number, public y: number, public w: number, public h: number, public l: number = 0) {
|
||||
this.o = [];
|
||||
this.q = null;
|
||||
}
|
||||
|
||||
split() {
|
||||
let t = this,
|
||||
x = t.x,
|
||||
y = t.y,
|
||||
w = t.w / 2,
|
||||
h = t.h / 2,
|
||||
l = t.l + 1;
|
||||
|
||||
t.q = [
|
||||
// top right
|
||||
new Quadtree(x + w, y, w, h, l),
|
||||
// top left
|
||||
new Quadtree(x, y, w, h, l),
|
||||
// bottom left
|
||||
new Quadtree(x, y + h, w, h, l),
|
||||
// bottom right
|
||||
new Quadtree(x + w, y + h, w, h, l),
|
||||
];
|
||||
}
|
||||
|
||||
// invokes callback with index of each overlapping quad
|
||||
quads(x: number, y: number, w: number, h: number, cb: (q: Quadtree) => void) {
|
||||
let t = this,
|
||||
q = t.q!,
|
||||
hzMid = t.x + t.w / 2,
|
||||
vtMid = t.y + t.h / 2,
|
||||
startIsNorth = y < vtMid,
|
||||
startIsWest = x < hzMid,
|
||||
endIsEast = x + w > hzMid,
|
||||
endIsSouth = y + h > vtMid;
|
||||
|
||||
// top-right quad
|
||||
startIsNorth && endIsEast && cb(q[0]);
|
||||
// top-left quad
|
||||
startIsWest && startIsNorth && cb(q[1]);
|
||||
// bottom-left quad
|
||||
startIsWest && endIsSouth && cb(q[2]);
|
||||
// bottom-right quad
|
||||
endIsEast && endIsSouth && cb(q[3]);
|
||||
}
|
||||
|
||||
add(o: Rect) {
|
||||
let t = this;
|
||||
|
||||
if (t.q != null) {
|
||||
t.quads(o.x, o.y, o.w, o.h, (q) => {
|
||||
q.add(o);
|
||||
});
|
||||
} else {
|
||||
let os = t.o;
|
||||
|
||||
os.push(o);
|
||||
|
||||
if (os.length > MAX_OBJECTS && t.l < MAX_LEVELS) {
|
||||
t.split();
|
||||
|
||||
for (let i = 0; i < os.length; i++) {
|
||||
let oi = os[i];
|
||||
|
||||
t.quads(oi.x, oi.y, oi.w, oi.h, (q) => {
|
||||
q.add(oi);
|
||||
});
|
||||
}
|
||||
|
||||
t.o.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get(x: number, y: number, w: number, h: number, cb: (o: Rect) => void) {
|
||||
let t = this;
|
||||
let os = t.o;
|
||||
|
||||
for (let i = 0; i < os.length; i++) {
|
||||
cb(os[i]);
|
||||
}
|
||||
|
||||
if (t.q != null) {
|
||||
t.quads(x, y, w, h, (q) => {
|
||||
q.get(x, y, w, h, cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.o.length = 0;
|
||||
this.q = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
import { AxisConfig, GraphGradientMode, HideableFieldConfig } from '../uPlot/config';
|
||||
import { VizLegendOptions } from '../VizLegend/types';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export enum BarStackingMode {
|
||||
None = 'none',
|
||||
Standard = 'standard',
|
||||
Percent = 'percent',
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export enum BarValueVisibility {
|
||||
Auto = 'auto',
|
||||
Never = 'never',
|
||||
Always = 'always',
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface BarChartOptions {
|
||||
orientation: VizOrientation;
|
||||
legend: VizLegendOptions;
|
||||
stacking: BarStackingMode;
|
||||
showValue: BarValueVisibility;
|
||||
barWidth: number;
|
||||
groupWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface BarChartFieldConfig extends AxisConfig, HideableFieldConfig {
|
||||
lineWidth?: number; // 0
|
||||
fillOpacity?: number; // 100
|
||||
gradientMode?: GraphGradientMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const defaultBarChartFieldConfig: BarChartFieldConfig = {
|
||||
lineWidth: 1,
|
||||
fillOpacity: 80,
|
||||
gradientMode: GraphGradientMode.None,
|
||||
axisSoftMin: 0,
|
||||
};
|
||||
@@ -18,7 +18,14 @@ import {
|
||||
import { useTheme } from '../../themes';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { PlotProps } from '../uPlot/types';
|
||||
import { AxisPlacement, DrawStyle, GraphFieldConfig, PointVisibility } from '../uPlot/config';
|
||||
import {
|
||||
AxisPlacement,
|
||||
DrawStyle,
|
||||
GraphFieldConfig,
|
||||
PointVisibility,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
} from '../uPlot/config';
|
||||
import { VizLayout } from '../VizLayout/VizLayout';
|
||||
import { LegendDisplayMode, VizLegendItem, VizLegendOptions } from '../VizLegend/types';
|
||||
import { VizLegend } from '../VizLegend/VizLegend';
|
||||
@@ -123,6 +130,8 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
if (xField.type === FieldType.time) {
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: true,
|
||||
range: () => {
|
||||
const r = currentTimeRange.current!;
|
||||
@@ -141,6 +150,8 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
// Not time!
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
@@ -170,18 +181,20 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
const scaleColor = getFieldSeriesColor(field, theme);
|
||||
const seriesColor = scaleColor.color;
|
||||
|
||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
||||
// The builder will manage unique scaleKeys and combine where appropriate
|
||||
builder.addScale({
|
||||
scaleKey,
|
||||
distribution: customConfig.scaleDistribution?.type,
|
||||
log: customConfig.scaleDistribution?.log,
|
||||
min: field.config.min,
|
||||
max: field.config.max,
|
||||
softMin: customConfig.axisSoftMin,
|
||||
softMax: customConfig.axisSoftMax,
|
||||
});
|
||||
// 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,
|
||||
|
||||
@@ -10,7 +10,14 @@ import {
|
||||
FieldConfig,
|
||||
getFieldDisplayName,
|
||||
} from '@grafana/data';
|
||||
import { AxisPlacement, DrawStyle, GraphFieldConfig, PointVisibility } from '../uPlot/config';
|
||||
import {
|
||||
AxisPlacement,
|
||||
DrawStyle,
|
||||
GraphFieldConfig,
|
||||
PointVisibility,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
} from '../uPlot/config';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { Themeable } from '../../types';
|
||||
@@ -91,6 +98,8 @@ export class Sparkline extends PureComponent<Props, State> {
|
||||
const xField = data.fields[0];
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: false, //xField.type === FieldType.time,
|
||||
range: () => {
|
||||
const { sparkline } = this.props;
|
||||
@@ -124,7 +133,13 @@ export class Sparkline extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
const scaleKey = config.unit || '__fixed';
|
||||
builder.addScale({ scaleKey, min: field.config.min, max: field.config.max });
|
||||
builder.addScale({
|
||||
scaleKey,
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
min: field.config.min,
|
||||
max: field.config.max,
|
||||
});
|
||||
builder.addAxis({
|
||||
scaleKey,
|
||||
theme,
|
||||
|
||||
@@ -3,6 +3,9 @@ import { LegendProps, LegendDisplayMode } from './types';
|
||||
import { VizLegendTable } from './VizLegendTable';
|
||||
import { VizLegendList } from './VizLegendList';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const VizLegend: React.FunctionComponent<LegendProps> = ({
|
||||
items,
|
||||
displayMode,
|
||||
|
||||
@@ -9,6 +9,9 @@ import { VizLegendListItem } from './VizLegendListItem';
|
||||
|
||||
export interface Props extends VizLegendBaseProps {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const VizLegendList: React.FunctionComponent<Props> = ({
|
||||
items,
|
||||
itemRenderer,
|
||||
|
||||
@@ -14,6 +14,9 @@ export interface Props {
|
||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const VizLegendListItem: React.FunctionComponent<Props> = ({ item, onSeriesColorChange, onLabelClick }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ interface Props {
|
||||
onColorChange: (color: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const VizLegendSeriesIcon: React.FunctionComponent<Props> = ({ disabled, color, onColorChange }) => {
|
||||
return disabled ? (
|
||||
<SeriesIcon color={color} />
|
||||
|
||||
@@ -18,6 +18,9 @@ const VizLegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ st
|
||||
|
||||
VizLegendItemStat.displayName = 'VizLegendItemStat';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const VizLegendStatsList: React.FunctionComponent<{ stats: DisplayValue[] }> = ({ stats }) => {
|
||||
if (stats.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -8,6 +8,9 @@ import sortBy from 'lodash/sortBy';
|
||||
import { LegendTableItem } from './VizLegendTableItem';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const VizLegendTable: FC<VizLegendTableProps> = ({
|
||||
items,
|
||||
sortBy: sortKey,
|
||||
|
||||
@@ -15,6 +15,9 @@ export interface Props {
|
||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const LegendTableItem: React.FunctionComponent<Props> = ({
|
||||
item,
|
||||
onSeriesColorChange,
|
||||
|
||||
@@ -207,5 +207,7 @@ export * from './uPlot/plugins';
|
||||
export { useRefreshAfterGraphRendered } from './uPlot/hooks';
|
||||
export { usePlotContext, usePlotData, usePlotPluginContext } from './uPlot/context';
|
||||
export { GraphNG, FIXED_UNIT } from './GraphNG/GraphNG';
|
||||
export { BarChart } from './BarChart/BarChart';
|
||||
export { BarChartOptions, BarStackingMode, BarValueVisibility, BarChartFieldConfig } from './BarChart/types';
|
||||
export { GraphNGLegendEvent, GraphNGLegendEventMode } from './GraphNG/types';
|
||||
export * from './NodeGraph';
|
||||
|
||||
@@ -46,6 +46,26 @@ export enum LineInterpolation {
|
||||
export enum ScaleDistribution {
|
||||
Linear = 'linear',
|
||||
Logarithmic = 'log',
|
||||
Ordinal = 'ordinal',
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export enum ScaleOrientation {
|
||||
Horizontal = 0,
|
||||
Vertical = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
|
||||
export enum ScaleDirection {
|
||||
Up = 1,
|
||||
Right = 1,
|
||||
Down = -1,
|
||||
Left = -1,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,10 +149,16 @@ export interface HideSeriesConfig {
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig {
|
||||
export interface HideableFieldConfig {
|
||||
hideFrom?: HideSeriesConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig, HideableFieldConfig {
|
||||
drawStyle?: DrawStyle;
|
||||
gradientMode?: GraphGradientMode;
|
||||
hideFrom?: HideSeriesConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,9 +11,12 @@ export interface AxisProps {
|
||||
label?: string;
|
||||
show?: boolean;
|
||||
size?: number | null;
|
||||
gap?: number;
|
||||
placement?: AxisPlacement;
|
||||
grid?: boolean;
|
||||
ticks?: boolean;
|
||||
formatValue?: (v: any) => string;
|
||||
splits?: Axis.Splits;
|
||||
values?: any;
|
||||
isTime?: boolean;
|
||||
timeZone?: TimeZone;
|
||||
@@ -37,7 +40,10 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
show = true,
|
||||
placement = AxisPlacement.Auto,
|
||||
grid = true,
|
||||
ticks = true,
|
||||
gap = 5,
|
||||
formatValue,
|
||||
splits,
|
||||
values,
|
||||
isTime,
|
||||
timeZone,
|
||||
@@ -54,16 +60,18 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
font: `12px 'Roboto'`,
|
||||
labelFont: `12px 'Roboto'`,
|
||||
size: this.props.size ?? calculateAxisSize,
|
||||
gap,
|
||||
grid: {
|
||||
show: grid,
|
||||
stroke: gridColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
},
|
||||
ticks: {
|
||||
show: true,
|
||||
show: ticks,
|
||||
stroke: gridColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
},
|
||||
splits,
|
||||
values: values,
|
||||
space: calculateSpace,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
import { UPlotConfigBuilder } from './UPlotConfigBuilder';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { GraphGradientMode, AxisPlacement, DrawStyle, PointVisibility, ScaleDistribution } from '../config';
|
||||
import {
|
||||
GraphGradientMode,
|
||||
AxisPlacement,
|
||||
DrawStyle,
|
||||
PointVisibility,
|
||||
ScaleDistribution,
|
||||
ScaleOrientation,
|
||||
ScaleDirection,
|
||||
} from '../config';
|
||||
import darkTheme from '../../../themes/dark';
|
||||
|
||||
describe('UPlotConfigBuilder', () => {
|
||||
@@ -26,7 +34,9 @@ describe('UPlotConfigBuilder', () => {
|
||||
"width": [Function],
|
||||
},
|
||||
},
|
||||
"hooks": Object {},
|
||||
"scales": Object {},
|
||||
"select": undefined,
|
||||
"series": Array [
|
||||
Object {},
|
||||
],
|
||||
@@ -41,11 +51,15 @@ describe('UPlotConfigBuilder', () => {
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'scale-x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: true,
|
||||
});
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'scale-y',
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
isTime: false,
|
||||
});
|
||||
|
||||
@@ -66,20 +80,26 @@ describe('UPlotConfigBuilder', () => {
|
||||
"width": [Function],
|
||||
},
|
||||
},
|
||||
"hooks": Object {},
|
||||
"scales": Object {
|
||||
"scale-x": Object {
|
||||
"auto": false,
|
||||
"dir": 1,
|
||||
"ori": 0,
|
||||
"range": [Function],
|
||||
"time": true,
|
||||
},
|
||||
"scale-y": Object {
|
||||
"auto": true,
|
||||
"dir": 1,
|
||||
"distr": 1,
|
||||
"log": undefined,
|
||||
"ori": 1,
|
||||
"range": [Function],
|
||||
"time": false,
|
||||
},
|
||||
},
|
||||
"select": undefined,
|
||||
"series": Array [
|
||||
Object {},
|
||||
],
|
||||
@@ -92,11 +112,15 @@ describe('UPlotConfigBuilder', () => {
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'scale-x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: true,
|
||||
});
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'scale-x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: false,
|
||||
});
|
||||
|
||||
@@ -109,6 +133,8 @@ describe('UPlotConfigBuilder', () => {
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'scale-y',
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
isTime: false,
|
||||
distribution: ScaleDistribution.Linear,
|
||||
});
|
||||
@@ -129,15 +155,19 @@ describe('UPlotConfigBuilder', () => {
|
||||
"width": [Function],
|
||||
},
|
||||
},
|
||||
"hooks": Object {},
|
||||
"scales": Object {
|
||||
"scale-y": Object {
|
||||
"auto": true,
|
||||
"dir": 1,
|
||||
"distr": 1,
|
||||
"log": undefined,
|
||||
"ori": 1,
|
||||
"range": [Function],
|
||||
"time": false,
|
||||
},
|
||||
},
|
||||
"select": undefined,
|
||||
"series": Array [
|
||||
Object {},
|
||||
],
|
||||
@@ -150,6 +180,8 @@ describe('UPlotConfigBuilder', () => {
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'scale-y',
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
isTime: false,
|
||||
distribution: ScaleDistribution.Linear,
|
||||
});
|
||||
@@ -171,15 +203,19 @@ describe('UPlotConfigBuilder', () => {
|
||||
"width": [Function],
|
||||
},
|
||||
},
|
||||
"hooks": Object {},
|
||||
"scales": Object {
|
||||
"scale-y": Object {
|
||||
"auto": true,
|
||||
"dir": 1,
|
||||
"distr": 1,
|
||||
"log": undefined,
|
||||
"ori": 1,
|
||||
"range": [Function],
|
||||
"time": false,
|
||||
},
|
||||
},
|
||||
"select": undefined,
|
||||
"series": Array [
|
||||
Object {},
|
||||
],
|
||||
@@ -192,6 +228,8 @@ describe('UPlotConfigBuilder', () => {
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'scale-y',
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
isTime: false,
|
||||
distribution: ScaleDistribution.Linear,
|
||||
log: 10,
|
||||
@@ -214,15 +252,19 @@ describe('UPlotConfigBuilder', () => {
|
||||
"width": [Function],
|
||||
},
|
||||
},
|
||||
"hooks": Object {},
|
||||
"scales": Object {
|
||||
"scale-y": Object {
|
||||
"auto": true,
|
||||
"dir": 1,
|
||||
"distr": 1,
|
||||
"log": undefined,
|
||||
"ori": 1,
|
||||
"range": [Function],
|
||||
"time": false,
|
||||
},
|
||||
},
|
||||
"select": undefined,
|
||||
"series": Array [
|
||||
Object {},
|
||||
],
|
||||
@@ -254,6 +296,7 @@ describe('UPlotConfigBuilder', () => {
|
||||
"axes": Array [
|
||||
Object {
|
||||
"font": "12px 'Roboto'",
|
||||
"gap": 5,
|
||||
"grid": Object {
|
||||
"show": false,
|
||||
"stroke": "#ffffff",
|
||||
@@ -267,6 +310,7 @@ describe('UPlotConfigBuilder', () => {
|
||||
"side": 2,
|
||||
"size": [Function],
|
||||
"space": [Function],
|
||||
"splits": undefined,
|
||||
"stroke": "gray",
|
||||
"ticks": Object {
|
||||
"show": true,
|
||||
@@ -291,7 +335,9 @@ describe('UPlotConfigBuilder', () => {
|
||||
"width": [Function],
|
||||
},
|
||||
},
|
||||
"hooks": Object {},
|
||||
"scales": Object {},
|
||||
"select": undefined,
|
||||
"series": Array [
|
||||
Object {},
|
||||
],
|
||||
@@ -410,7 +456,9 @@ describe('UPlotConfigBuilder', () => {
|
||||
"width": [Function],
|
||||
},
|
||||
},
|
||||
"hooks": Object {},
|
||||
"scales": Object {},
|
||||
"select": undefined,
|
||||
"series": Array [
|
||||
Object {},
|
||||
Object {
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import { PlotSeriesConfig } from '../types';
|
||||
import { PlotConfig } from '../types';
|
||||
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
|
||||
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
||||
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
||||
import { AxisPlacement } from '../config';
|
||||
import { Cursor, Band } from 'uplot';
|
||||
import { Cursor, Band, Hooks, BBox } from 'uplot';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
|
||||
type valueof<T> = T[keyof T];
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
private series: UPlotSeriesBuilder[] = [];
|
||||
private axes: Record<string, UPlotAxisBuilder> = {};
|
||||
private scales: UPlotScaleBuilder[] = [];
|
||||
private bands: Band[] = [];
|
||||
private cursor: Cursor | undefined;
|
||||
// uPlot types don't export the Select interface prior to 1.6.4
|
||||
private select: Partial<BBox> | undefined;
|
||||
private hasLeftAxis = false;
|
||||
private hasBottomAxis = false;
|
||||
private hooks: Hooks.Arrays = {};
|
||||
|
||||
addHook(type: keyof Hooks.Defs, hook: valueof<Hooks.Defs>) {
|
||||
if (!this.hooks[type]) {
|
||||
this.hooks[type] = [];
|
||||
}
|
||||
|
||||
this.hooks[type]!.push(hook as any);
|
||||
}
|
||||
|
||||
addAxis(props: AxisProps) {
|
||||
props.placement = props.placement ?? AxisPlacement.Auto;
|
||||
@@ -54,6 +67,11 @@ export class UPlotConfigBuilder {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
// uPlot types don't export the Select interface prior to 1.6.4
|
||||
setSelect(select: Partial<BBox>) {
|
||||
this.select = select;
|
||||
}
|
||||
|
||||
addSeries(props: SeriesProps) {
|
||||
this.series.push(new UPlotSeriesBuilder(props));
|
||||
}
|
||||
@@ -77,13 +95,19 @@ export class UPlotConfigBuilder {
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
const config: PlotSeriesConfig = { series: [{}] };
|
||||
const config: PlotConfig = { series: [{}] };
|
||||
config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map((a) => a.getConfig());
|
||||
config.series = [...config.series, ...this.series.map((s) => s.getConfig())];
|
||||
config.scales = this.scales.reduce((acc, s) => {
|
||||
return { ...acc, ...s.getConfig() };
|
||||
}, {});
|
||||
|
||||
config.hooks = this.hooks;
|
||||
|
||||
/* @ts-ignore */
|
||||
// uPlot types don't export the Select interface prior to 1.6.4
|
||||
config.select = this.select;
|
||||
|
||||
config.cursor = this.cursor || {};
|
||||
|
||||
// When bands exist, only keep fill when defined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import uPlot, { Scale, Range } from 'uplot';
|
||||
import { PlotConfigBuilder } from '../types';
|
||||
import { ScaleDistribution } from '../config';
|
||||
import { ScaleDistribution, ScaleOrientation, ScaleDirection } from '../config';
|
||||
|
||||
export interface ScaleProps {
|
||||
scaleKey: string;
|
||||
@@ -9,8 +9,10 @@ export interface ScaleProps {
|
||||
max?: number | null;
|
||||
softMin?: number | null;
|
||||
softMax?: number | null;
|
||||
range?: () => number[]; // min/max
|
||||
range?: Scale.Range;
|
||||
distribution?: ScaleDistribution;
|
||||
orientation: ScaleOrientation;
|
||||
direction: ScaleDirection;
|
||||
log?: number;
|
||||
}
|
||||
|
||||
@@ -21,10 +23,25 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
const { isTime, scaleKey, min: hardMin, max: hardMax, softMin, softMax, range } = this.props;
|
||||
const {
|
||||
isTime,
|
||||
scaleKey,
|
||||
min: hardMin,
|
||||
max: hardMax,
|
||||
softMin,
|
||||
softMax,
|
||||
range,
|
||||
direction,
|
||||
orientation,
|
||||
} = this.props;
|
||||
const distribution = !isTime
|
||||
? {
|
||||
distr: this.props.distribution === ScaleDistribution.Logarithmic ? 3 : 1,
|
||||
distr:
|
||||
this.props.distribution === ScaleDistribution.Logarithmic
|
||||
? 3
|
||||
: this.props.distribution === ScaleDistribution.Ordinal
|
||||
? 2
|
||||
: 1,
|
||||
log: this.props.distribution === ScaleDistribution.Logarithmic ? this.props.log || 2 : undefined,
|
||||
}
|
||||
: {};
|
||||
@@ -57,7 +74,7 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
|
||||
let hardMinOnly = softMin == null && hardMin != null;
|
||||
let hardMaxOnly = softMax == null && hardMax != null;
|
||||
|
||||
if (scale.distr === 1) {
|
||||
if (scale.distr === 1 || scale.distr === 2) {
|
||||
// @ts-ignore here we may use hardMin / hardMax to make sure any extra padding is computed from a more accurate delta
|
||||
minMax = uPlot.rangeNum(hardMinOnly ? hardMin : dataMin, hardMaxOnly ? hardMax : dataMax, rangeConfig);
|
||||
} else if (scale.distr === 3) {
|
||||
@@ -81,6 +98,8 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
|
||||
time: isTime,
|
||||
auto: !isTime,
|
||||
range: range ?? rangeFn,
|
||||
dir: direction,
|
||||
ori: orientation,
|
||||
...distribution,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,9 @@ export interface SeriesProps extends LineConfig, FillConfig, PointsConfig {
|
||||
/** Used when gradientMode is set to Scheme */
|
||||
colorMode?: FieldColorMode;
|
||||
fieldName: string;
|
||||
drawStyle: DrawStyle;
|
||||
drawStyle?: DrawStyle;
|
||||
pathBuilder?: Series.PathBuilder;
|
||||
pointsBuilder?: Series.Points.Show;
|
||||
show?: boolean;
|
||||
dataFrameFieldIndex?: DataFrameFieldIndex;
|
||||
hideInLegend?: boolean;
|
||||
@@ -33,6 +35,8 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
||||
getConfig() {
|
||||
const {
|
||||
drawStyle,
|
||||
pathBuilder,
|
||||
pointsBuilder,
|
||||
lineInterpolation,
|
||||
lineWidth,
|
||||
lineStyle,
|
||||
@@ -46,9 +50,13 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
||||
|
||||
let lineConfig: Partial<Series> = {};
|
||||
|
||||
if (drawStyle === DrawStyle.Points) {
|
||||
if (pathBuilder != null) {
|
||||
lineConfig.paths = pathBuilder;
|
||||
lineConfig.stroke = this.getLineColor();
|
||||
lineConfig.width = lineWidth;
|
||||
} else if (drawStyle === DrawStyle.Points) {
|
||||
lineConfig.paths = () => null;
|
||||
} else {
|
||||
} else if (drawStyle != null) {
|
||||
lineConfig.stroke = this.getLineColor();
|
||||
lineConfig.width = lineWidth;
|
||||
if (lineStyle && lineStyle.fill !== 'solid') {
|
||||
@@ -71,18 +79,22 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
||||
},
|
||||
};
|
||||
|
||||
// we cannot set points.show property above (even to undefined) as that will clear uPlot's default auto behavior
|
||||
if (drawStyle === DrawStyle.Points) {
|
||||
pointsConfig.points!.show = true;
|
||||
if (pointsBuilder != null) {
|
||||
pointsConfig.points!.show = pointsBuilder;
|
||||
} else {
|
||||
if (showPoints === PointVisibility.Auto) {
|
||||
if (drawStyle === DrawStyle.Bars) {
|
||||
pointsConfig.points!.show = false;
|
||||
}
|
||||
} else if (showPoints === PointVisibility.Never) {
|
||||
pointsConfig.points!.show = false;
|
||||
} else if (showPoints === PointVisibility.Always) {
|
||||
// we cannot set points.show property above (even to undefined) as that will clear uPlot's default auto behavior
|
||||
if (drawStyle === DrawStyle.Points) {
|
||||
pointsConfig.points!.show = true;
|
||||
} else {
|
||||
if (showPoints === PointVisibility.Auto) {
|
||||
if (drawStyle === DrawStyle.Bars) {
|
||||
pointsConfig.points!.show = false;
|
||||
}
|
||||
} else if (showPoints === PointVisibility.Never) {
|
||||
pointsConfig.points!.show = false;
|
||||
} else if (showPoints === PointVisibility.Always) {
|
||||
pointsConfig.points!.show = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import uPlot, { Options, Hooks } from 'uplot';
|
||||
import { DataFrame, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
|
||||
|
||||
export type PlotSeriesConfig = Pick<Options, 'series' | 'scales' | 'axes' | 'cursor' | 'bands'>;
|
||||
export type PlotConfig = Pick<Options, 'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select'>;
|
||||
|
||||
export type PlotPlugin = {
|
||||
id: string;
|
||||
/** can mutate provided opts as necessary */
|
||||
|
||||
@@ -18,7 +18,7 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
|
||||
|
||||
const renderEditor = useCallback(
|
||||
(item: FieldConfigPropertyItem, categoryItemCount: number) => {
|
||||
if (item.isCustom && item.showIf && !item.showIf(config.defaults.custom)) {
|
||||
if (item.isCustom && item.showIf && !item.showIf(config.defaults.custom, data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({
|
||||
{Object.keys(optionEditors).map((c, i) => {
|
||||
const optionsToShow = optionEditors[c]
|
||||
.map((e, j) => {
|
||||
if (e.showIf && !e.showIf(options)) {
|
||||
if (e.showIf && !e.showIf(options, data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
import * as pieChartPanel from 'app/plugins/panel/piechart/module';
|
||||
import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
|
||||
import * as barChartPanel from 'app/plugins/panel/barchart/module';
|
||||
import * as logsPanel from 'app/plugins/panel/logs/module';
|
||||
import * as newsPanel from 'app/plugins/panel/news/module';
|
||||
import * as livePanel from 'app/plugins/panel/live/module';
|
||||
@@ -101,6 +102,7 @@ const builtInPlugins: any = {
|
||||
'app/plugins/panel/gauge/module': gaugePanel,
|
||||
'app/plugins/panel/piechart/module': pieChartPanel,
|
||||
'app/plugins/panel/bargauge/module': barGaugePanel,
|
||||
'app/plugins/panel/barchart/module': barChartPanel,
|
||||
'app/plugins/panel/logs/module': logsPanel,
|
||||
'app/plugins/panel/welcome/module': welcomeBanner,
|
||||
'app/plugins/panel/nodeGraph/module': nodeGraph,
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { DataFrame, Field, FieldType, PanelProps } from '@grafana/data';
|
||||
import { BarChart, BarChartOptions, GraphNGLegendEvent } from '@grafana/ui';
|
||||
import { changeSeriesColorConfigFactory } from '../timeseries/overrides/colorSeriesConfigFactory';
|
||||
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
interface Props extends PanelProps<BarChartOptions> {}
|
||||
|
||||
interface BarData {
|
||||
error?: string;
|
||||
frame?: DataFrame; // first string vs all numbers
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const BarChartPanel: React.FunctionComponent<Props> = ({
|
||||
data,
|
||||
options,
|
||||
width,
|
||||
height,
|
||||
fieldConfig,
|
||||
onFieldConfigChange,
|
||||
}) => {
|
||||
if (!data || !data.series?.length) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>No data found in response</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const onLegendClick = useCallback(
|
||||
(event: GraphNGLegendEvent) => {
|
||||
onFieldConfigChange(hideSeriesConfigFactory(event, fieldConfig, data.series));
|
||||
},
|
||||
[fieldConfig, onFieldConfigChange, data.series]
|
||||
);
|
||||
|
||||
const onSeriesColorChange = useCallback(
|
||||
(label: string, color: string) => {
|
||||
onFieldConfigChange(changeSeriesColorConfigFactory(label, color, fieldConfig));
|
||||
},
|
||||
[fieldConfig, onFieldConfigChange]
|
||||
);
|
||||
|
||||
const barData = useMemo<BarData>(() => {
|
||||
const firstFrame = data.series[0];
|
||||
const firstString = firstFrame.fields.find((f) => f.type === FieldType.string);
|
||||
if (!firstString) {
|
||||
return {
|
||||
error: 'Bar charts requires a string field',
|
||||
};
|
||||
}
|
||||
const fields: Field[] = [firstString];
|
||||
for (const f of firstFrame.fields) {
|
||||
if (f.type === FieldType.number) {
|
||||
fields.push(f);
|
||||
}
|
||||
}
|
||||
if (fields.length < 2) {
|
||||
return {
|
||||
error: 'No numeric fields found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
frame: {
|
||||
...firstFrame,
|
||||
fields, // filtered to to the values we have
|
||||
},
|
||||
};
|
||||
}, [width, height, options, data]);
|
||||
|
||||
if (barData.error) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>{barData.error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!barData.frame) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>No data found in response</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
data={barData.frame}
|
||||
width={width}
|
||||
height={height}
|
||||
theme={config.theme}
|
||||
onLegendClick={onLegendClick}
|
||||
onSeriesColorChange={onSeriesColorChange}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 78.59 80.42"><defs><style>.cls-1{fill:#3865ab;}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:url(#linear-gradient-2);}.cls-4{fill:url(#linear-gradient-3);}</style><linearGradient id="linear-gradient" x1="15.72" y1="68.76" x2="15.43" y2="49.22" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient><linearGradient id="linear-gradient-2" x1="45.26" y1="68.7" x2="44.55" y2="24.23" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="73.92" y1="67.1" x2="74.48" y2="8.07" xlink:href="#linear-gradient"/></defs><g id="Layer_2" data-name="Layer 2"><g id="Icons"><rect class="cls-1" y="33.91" width="8.74" height="38.3" rx="1"/><rect class="cls-1" x="29.31" y="39.5" width="8.74" height="32.72" rx="1"/><rect class="cls-1" y="76.42" width="78.59" height="4" rx="1"/><rect class="cls-1" x="58.62" y="9.13" width="8.74" height="63.08" rx="1"/><rect class="cls-2" x="11.22" y="48" width="8.74" height="24.21" rx="1"/><rect class="cls-3" x="40.53" y="20.1" width="8.74" height="52.12" rx="1"/><rect class="cls-4" x="69.84" width="8.74" height="72.21" rx="1"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Executable
+150
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
DataFrame,
|
||||
FieldColorModeId,
|
||||
FieldConfigProperty,
|
||||
FieldType,
|
||||
PanelPlugin,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
import { BarChartPanel } from './BarChartPanel';
|
||||
import {
|
||||
BarChartFieldConfig,
|
||||
BarChartOptions,
|
||||
BarStackingMode,
|
||||
BarValueVisibility,
|
||||
graphFieldOptions,
|
||||
} from '@grafana/ui';
|
||||
import { addAxisConfig, addHideFrom, addLegendOptions } from '../timeseries/config';
|
||||
import { defaultBarChartFieldConfig } from '@grafana/ui/src/components/BarChart/types';
|
||||
|
||||
export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarChartPanel)
|
||||
.useFieldConfig({
|
||||
standardOptions: {
|
||||
[FieldConfigProperty.Color]: {
|
||||
settings: {
|
||||
byValueSupport: false,
|
||||
},
|
||||
defaultValue: {
|
||||
mode: FieldColorModeId.PaletteClassic,
|
||||
},
|
||||
},
|
||||
},
|
||||
useCustomConfig: (builder) => {
|
||||
const cfg = defaultBarChartFieldConfig;
|
||||
|
||||
builder
|
||||
.addSliderInput({
|
||||
path: 'lineWidth',
|
||||
name: 'Line width',
|
||||
defaultValue: cfg.lineWidth,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'fillOpacity',
|
||||
name: 'Fill opacity',
|
||||
defaultValue: cfg.fillOpacity,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
})
|
||||
.addRadio({
|
||||
path: 'gradientMode',
|
||||
name: 'Gradient mode',
|
||||
defaultValue: graphFieldOptions.fillGradient[0].value,
|
||||
settings: {
|
||||
options: graphFieldOptions.fillGradient,
|
||||
},
|
||||
});
|
||||
|
||||
addAxisConfig(builder, cfg, true);
|
||||
addHideFrom(builder);
|
||||
},
|
||||
})
|
||||
.setPanelOptions((builder) => {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'orientation',
|
||||
name: 'Orientation',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VizOrientation.Auto, label: 'Auto' },
|
||||
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
|
||||
{ value: VizOrientation.Vertical, label: 'Vertical' },
|
||||
],
|
||||
},
|
||||
defaultValue: VizOrientation.Auto,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'stacking',
|
||||
name: 'Stacking',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: BarStackingMode.None, label: 'None' },
|
||||
{ value: BarStackingMode.Standard, label: 'Standard' },
|
||||
{ value: BarStackingMode.Percent, label: 'Percent' },
|
||||
],
|
||||
},
|
||||
defaultValue: BarStackingMode.None,
|
||||
showIf: () => false, // <<< Hide from the UI for now
|
||||
})
|
||||
.addRadio({
|
||||
path: 'showValue',
|
||||
name: 'Show values',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: BarValueVisibility.Auto, label: 'Auto' },
|
||||
{ value: BarValueVisibility.Always, label: 'Always' },
|
||||
{ value: BarValueVisibility.Never, label: 'Never' },
|
||||
],
|
||||
},
|
||||
defaultValue: BarValueVisibility.Auto,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'groupWidth',
|
||||
name: 'Group width',
|
||||
defaultValue: 0.7,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
showIf: (c, data) => {
|
||||
if (c.stacking && c.stacking !== BarStackingMode.None) {
|
||||
return false;
|
||||
}
|
||||
return countNumberFields(data) !== 1;
|
||||
},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'barWidth',
|
||||
name: 'Bar width',
|
||||
defaultValue: 0.97,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
});
|
||||
|
||||
addLegendOptions(builder);
|
||||
});
|
||||
|
||||
function countNumberFields(data?: DataFrame[]): number {
|
||||
let count = 0;
|
||||
if (data) {
|
||||
for (const frame of data) {
|
||||
for (const field of frame.fields) {
|
||||
if (field.type === FieldType.number) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Bar chart",
|
||||
"id": "barchart",
|
||||
|
||||
"state": "alpha",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/barchart.svg",
|
||||
"large": "img/barchart.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FieldOverrideContext,
|
||||
getFieldDisplayName,
|
||||
escapeStringForRegex,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
|
||||
@@ -101,12 +102,12 @@ export function addStandardDataReduceOptions(
|
||||
description: 'Stacking direction in case of multiple series or fields',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'horizontal', label: 'Horizontal' },
|
||||
{ value: 'vertical', label: 'Vertical' },
|
||||
{ value: VizOrientation.Auto, label: 'Auto' },
|
||||
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
|
||||
{ value: VizOrientation.Vertical, label: 'Vertical' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'auto',
|
||||
defaultValue: VizOrientation.Auto,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
FieldColorModeId,
|
||||
FieldConfigEditorBuilder,
|
||||
FieldConfigProperty,
|
||||
FieldType,
|
||||
identityOverrideProcessor,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
ScaleDistributionConfig,
|
||||
GraphGradientMode,
|
||||
LegendDisplayMode,
|
||||
AxisConfig,
|
||||
} from '@grafana/ui';
|
||||
import { SeriesConfigEditor } from './HideSeriesConfigEditor';
|
||||
import { ScaleDistributionEditor } from './ScaleDistributionEditor';
|
||||
@@ -151,85 +153,103 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
|
||||
step: 1,
|
||||
},
|
||||
showIf: (c) => c.showPoints !== PointVisibility.Never || c.drawStyle === DrawStyle.Points,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'axisPlacement',
|
||||
name: 'Placement',
|
||||
category: ['Axis'],
|
||||
defaultValue: graphFieldOptions.axisPlacement[0].value,
|
||||
settings: {
|
||||
options: graphFieldOptions.axisPlacement,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'axisLabel',
|
||||
name: 'Label',
|
||||
category: ['Axis'],
|
||||
defaultValue: '',
|
||||
settings: {
|
||||
placeholder: 'Optional text',
|
||||
},
|
||||
showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden,
|
||||
// no matter what the field type is
|
||||
shouldApply: () => true,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'axisWidth',
|
||||
name: 'Width',
|
||||
category: ['Axis'],
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
},
|
||||
showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'axisSoftMin',
|
||||
name: 'Soft min',
|
||||
category: ['Axis'],
|
||||
settings: {
|
||||
placeholder: 'See: Standard options > Min',
|
||||
},
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'axisSoftMax',
|
||||
name: 'Soft max',
|
||||
category: ['Axis'],
|
||||
settings: {
|
||||
placeholder: 'See: Standard options > Max',
|
||||
},
|
||||
})
|
||||
.addCustomEditor<void, ScaleDistributionConfig>({
|
||||
id: 'scaleDistribution',
|
||||
path: 'scaleDistribution',
|
||||
name: 'Scale',
|
||||
category: ['Axis'],
|
||||
editor: ScaleDistributionEditor,
|
||||
override: ScaleDistributionEditor,
|
||||
defaultValue: { type: ScaleDistribution.Linear },
|
||||
shouldApply: (f) => f.type === FieldType.number,
|
||||
process: identityOverrideProcessor,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'hideFrom',
|
||||
name: 'Hide in area',
|
||||
category: ['Series'],
|
||||
path: 'hideFrom',
|
||||
defaultValue: {
|
||||
tooltip: false,
|
||||
graph: false,
|
||||
legend: false,
|
||||
},
|
||||
editor: SeriesConfigEditor,
|
||||
override: SeriesConfigEditor,
|
||||
shouldApply: () => true,
|
||||
hideFromDefaults: true,
|
||||
hideFromOverrides: true,
|
||||
process: (value) => value,
|
||||
});
|
||||
|
||||
addAxisConfig(builder, cfg);
|
||||
addHideFrom(builder);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function addHideFrom(builder: FieldConfigEditorBuilder<AxisConfig>) {
|
||||
builder.addCustomEditor({
|
||||
id: 'hideFrom',
|
||||
name: 'Hide in area',
|
||||
category: ['Series'],
|
||||
path: 'hideFrom',
|
||||
defaultValue: {
|
||||
tooltip: false,
|
||||
graph: false,
|
||||
legend: false,
|
||||
},
|
||||
editor: SeriesConfigEditor,
|
||||
override: SeriesConfigEditor,
|
||||
shouldApply: () => true,
|
||||
hideFromDefaults: true,
|
||||
hideFromOverrides: true,
|
||||
process: (value) => value,
|
||||
});
|
||||
}
|
||||
|
||||
export function addAxisConfig(
|
||||
builder: FieldConfigEditorBuilder<AxisConfig>,
|
||||
defaultConfig: AxisConfig,
|
||||
hideScale?: boolean
|
||||
) {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'axisPlacement',
|
||||
name: 'Placement',
|
||||
category: ['Axis'],
|
||||
defaultValue: graphFieldOptions.axisPlacement[0].value,
|
||||
settings: {
|
||||
options: graphFieldOptions.axisPlacement,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'axisLabel',
|
||||
name: 'Label',
|
||||
category: ['Axis'],
|
||||
defaultValue: '',
|
||||
settings: {
|
||||
placeholder: 'Optional text',
|
||||
},
|
||||
showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden,
|
||||
// no matter what the field type is
|
||||
shouldApply: () => true,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'axisWidth',
|
||||
name: 'Width',
|
||||
category: ['Axis'],
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
},
|
||||
showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'axisSoftMin',
|
||||
name: 'Soft min',
|
||||
defaultValue: defaultConfig.axisSoftMin,
|
||||
category: ['Axis'],
|
||||
settings: {
|
||||
placeholder: 'See: Standard options > Min',
|
||||
},
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'axisSoftMax',
|
||||
name: 'Soft max',
|
||||
defaultValue: defaultConfig.axisSoftMax,
|
||||
category: ['Axis'],
|
||||
settings: {
|
||||
placeholder: 'See: Standard options > Max',
|
||||
},
|
||||
});
|
||||
if (!hideScale) {
|
||||
builder.addCustomEditor<void, ScaleDistributionConfig>({
|
||||
id: 'scaleDistribution',
|
||||
path: 'scaleDistribution',
|
||||
name: 'Scale',
|
||||
category: ['Axis'],
|
||||
editor: ScaleDistributionEditor,
|
||||
override: ScaleDistributionEditor,
|
||||
defaultValue: { type: ScaleDistribution.Linear },
|
||||
shouldApply: (f) => f.type === FieldType.number,
|
||||
process: identityOverrideProcessor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function addLegendOptions(builder: PanelOptionsEditorBuilder<OptionsWithLegend>) {
|
||||
builder
|
||||
.addRadio({
|
||||
|
||||
Reference in New Issue
Block a user