Alpha panel: new Timeline/Discrete panel (#31973)

This commit is contained in:
Leon Sorokin
2021-04-06 18:06:46 -05:00
committed by GitHub
parent ea202513cd
commit 6082a9360e
15 changed files with 1389 additions and 5 deletions

View File

@@ -46,8 +46,8 @@ export interface BarsOptions {
groupWidth: number;
barWidth: number;
formatValue?: (seriesIdx: number, value: any) => string;
onHover?: (seriesIdx: number, valueIdx: any) => void;
onLeave?: (seriesIdx: number, valueIdx: any) => void;
onHover?: (seriesIdx: number, valueIdx: number) => void;
onLeave?: (seriesIdx: number, valueIdx: number) => void;
}
/**

View File

@@ -41,7 +41,10 @@ export interface GraphNGProps extends Themeable {
children?: React.ReactNode;
}
interface GraphNGState {
/**
* @internal -- not a public API
*/
export interface GraphNGState {
data: AlignedData;
alignedDataFrame: DataFrame;
dimFields: XYFieldMatchers;

View File

@@ -0,0 +1,195 @@
import React from 'react';
import { compareArrayValues, compareDataFrameStructures, FieldMatcherID, fieldMatchers } from '@grafana/data';
import { withTheme } from '../../themes';
import { GraphNGContext } from '../GraphNG/hooks';
import { GraphNGState } from '../GraphNG/GraphNG';
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; // << preparePlotConfigBuilder is really the only change vs GraphNG
import { preparePlotData } from '../uPlot/utils';
import { PlotLegend } from '../uPlot/PlotLegend';
import { UPlotChart } from '../uPlot/Plot';
import { LegendDisplayMode } from '../VizLegend/types';
import { VizLayout } from '../VizLayout/VizLayout';
import { TimelineProps } from './types';
class UnthemedTimeline extends React.Component<TimelineProps, GraphNGState> {
constructor(props: TimelineProps) {
super(props);
let dimFields = props.fields;
if (!dimFields) {
dimFields = {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}), // this may be either numeric or strings, (or bools?)
};
}
this.state = { dimFields } as GraphNGState;
}
/**
* Since no matter the nature of the change (data vs config only) we always calculate the plot-ready AlignedData array.
* It's cheaper than run prev and current AlignedData comparison to indicate necessity of data-only update. We assume
* that if there were no config updates, we can do data only updates(as described in Plot.tsx, L32)
*
* Preparing the uPlot-ready data in getDerivedStateFromProps makes the data updates happen only once for a render cycle.
* If we did it in componendDidUpdate we will end up having two data-only updates: 1) for props and 2) for state update
*
* This is a way of optimizing the uPlot rendering, yet there are consequences: when there is a config update,
* the data is updated first, and then the uPlot is re-initialized. But since the config updates does not happen that
* often (apart from the edit mode interactions) this should be a fair performance compromise.
*/
static getDerivedStateFromProps(props: TimelineProps, state: GraphNGState) {
let dimFields = props.fields;
if (!dimFields) {
dimFields = {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
};
}
const frame = preparePlotFrame(props.data, dimFields);
if (!frame) {
return { ...state, dimFields };
}
return {
...state,
data: preparePlotData(frame),
alignedDataFrame: frame,
seriesToDataFrameFieldIndexMap: frame.fields.map((f) => f.state!.origin!),
dimFields,
};
}
componentDidMount() {
const { theme, mode, rowHeight, colWidth, showValue } = this.props;
// alignedDataFrame is already prepared by getDerivedStateFromProps method
const { alignedDataFrame } = this.state;
if (!alignedDataFrame) {
return;
}
this.setState({
config: preparePlotConfigBuilder(alignedDataFrame, theme, this.getTimeRange, this.getTimeZone, {
mode,
rowHeight,
colWidth,
showValue,
}),
});
}
componentDidUpdate(prevProps: TimelineProps) {
const { data, theme, timeZone, mode, rowHeight, colWidth, showValue } = this.props;
const { alignedDataFrame } = this.state;
let shouldConfigUpdate = false;
let stateUpdate = {} as GraphNGState;
if (
this.state.config === undefined ||
timeZone !== prevProps.timeZone ||
mode !== prevProps.mode ||
rowHeight !== prevProps.rowHeight ||
colWidth !== prevProps.colWidth ||
showValue !== prevProps.showValue
) {
shouldConfigUpdate = true;
}
if (data !== prevProps.data) {
if (!alignedDataFrame) {
return;
}
if (!compareArrayValues(data, prevProps.data, compareDataFrameStructures)) {
shouldConfigUpdate = true;
}
}
if (shouldConfigUpdate) {
const builder = preparePlotConfigBuilder(alignedDataFrame, theme, this.getTimeRange, this.getTimeZone, {
mode,
rowHeight,
colWidth,
showValue,
});
stateUpdate = { ...stateUpdate, config: builder };
}
if (Object.keys(stateUpdate).length > 0) {
this.setState(stateUpdate);
}
}
mapSeriesIndexToDataFrameFieldIndex = (i: number) => {
return this.state.seriesToDataFrameFieldIndexMap[i];
};
getTimeRange = () => {
return this.props.timeRange;
};
getTimeZone = () => {
return this.props.timeZone;
};
renderLegend() {
const { legend, onSeriesColorChange, onLegendClick, data } = this.props;
const { config } = this.state;
if (!config || (legend && legend.displayMode === LegendDisplayMode.Hidden)) {
return;
}
return (
<PlotLegend
data={data}
config={config}
onSeriesColorChange={onSeriesColorChange}
onLegendClick={onLegendClick}
maxHeight="35%"
maxWidth="60%"
{...legend}
/>
);
}
render() {
const { width, height, children, timeZone, timeRange, ...plotProps } = this.props;
if (!this.state.data || !this.state.config) {
return null;
}
return (
<GraphNGContext.Provider
value={{
mapSeriesIndexToDataFrameFieldIndex: this.mapSeriesIndexToDataFrameFieldIndex,
dimFields: this.state.dimFields,
data: this.state.alignedDataFrame,
}}
>
<VizLayout width={width} height={height}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart
{...plotProps}
config={this.state.config!}
data={this.state.data}
width={vizWidth}
height={vizHeight}
timeRange={timeRange}
>
{children}
</UPlotChart>
)}
</VizLayout>
</GraphNGContext.Provider>
);
}
}
export const Timeline = withTheme(UnthemedTimeline);
Timeline.displayName = 'Timeline';

View File

@@ -0,0 +1,432 @@
import uPlot, { Series, Cursor } from 'uplot';
import { FIXED_UNIT } from '../GraphNG/GraphNG';
import { Quadtree, Rect, pointWithin } from '../BarChart/quadtree';
import { distribute, SPACE_BETWEEN } from '../BarChart/distribute';
import { TimelineMode } from './types';
import { TimeRange } from '@grafana/data';
import { BarValueVisibility } from '../BarChart/types';
const { round, min, ceil } = Math;
const pxRatio = devicePixelRatio;
const laneDistr = SPACE_BETWEEN;
const font = Math.round(10 * pxRatio) + 'px Roboto';
type WalkCb = (idx: number, offPx: number, dimPx: number) => void;
function walk(rowHeight: number, yIdx: number | null, count: number, dim: number, draw: WalkCb) {
distribute(count, rowHeight, laneDistr, yIdx, (i, offPct, dimPct) => {
let laneOffPx = dim * offPct;
let laneWidPx = dim * dimPct;
draw(i, laneOffPx, laneWidPx);
});
}
/**
* @internal
*/
export interface TimelineCoreOptions {
mode: TimelineMode;
numSeries: number;
rowHeight: number;
colWidth?: number;
showValue: BarValueVisibility;
isDiscrete: (seriesIdx: number) => boolean;
label: (seriesIdx: number) => string;
fill: (seriesIdx: number, valueIdx: number, value: any) => CanvasRenderingContext2D['fillStyle'];
stroke: (seriesIdx: number, valueIdx: number, value: any) => CanvasRenderingContext2D['strokeStyle'];
getTimeRange: () => TimeRange;
formatValue?: (seriesIdx: number, value: any) => string;
onHover?: (seriesIdx: number, valueIdx: number) => void;
onLeave?: (seriesIdx: number, valueIdx: number) => void;
}
/**
* @internal
*/
export function getConfig(opts: TimelineCoreOptions) {
const {
mode,
numSeries,
isDiscrete,
rowHeight = 0,
colWidth = 0,
showValue,
label,
fill,
stroke,
formatValue,
getTimeRange,
// onHover,
// onLeave,
} = opts;
let qt: Quadtree;
const hoverMarks = Array(numSeries)
.fill(null)
.map(() => {
let mark = document.createElement('div');
mark.classList.add('bar-mark');
mark.style.position = 'absolute';
mark.style.background = 'rgba(255,255,255,0.4)';
return mark;
});
const hovered: Array<Rect | null> = Array(numSeries).fill(null);
const size = [colWidth, 100];
const gapFactor = 1 - size[0];
const maxWidth = (size[1] ?? Infinity) * pxRatio;
const fillPaths: Map<CanvasRenderingContext2D['fillStyle'], Path2D> = new Map();
const strokePaths: Map<CanvasRenderingContext2D['strokeStyle'], Path2D> = new Map();
function drawBoxes(ctx: CanvasRenderingContext2D) {
fillPaths.forEach((fillPath, fillStyle) => {
ctx.fillStyle = fillStyle;
ctx.fill(fillPath);
});
strokePaths.forEach((strokePath, strokeStyle) => {
ctx.strokeStyle = strokeStyle;
ctx.stroke(strokePath);
});
fillPaths.clear();
strokePaths.clear();
}
function putBox(
ctx: CanvasRenderingContext2D,
rect: uPlot.RectH,
xOff: number,
yOff: number,
lft: number,
top: number,
wid: number,
hgt: number,
strokeWidth: number,
seriesIdx: number,
valueIdx: number,
value: any,
discrete: boolean
) {
if (discrete) {
let fillStyle = fill(seriesIdx + 1, valueIdx, value);
let fillPath = fillPaths.get(fillStyle);
if (fillPath == null) {
fillPaths.set(fillStyle, (fillPath = new Path2D()));
}
rect(fillPath, lft, top, wid, hgt);
if (strokeWidth) {
let strokeStyle = stroke(seriesIdx + 1, valueIdx, value);
let strokePath = strokePaths.get(strokeStyle);
if (strokePath == null) {
strokePaths.set(strokeStyle, (strokePath = new Path2D()));
}
rect(strokePath, lft + strokeWidth / 2, top + strokeWidth / 2, wid - strokeWidth, hgt - strokeWidth);
}
} else {
ctx.beginPath();
rect(ctx, lft, top, wid, hgt);
ctx.fillStyle = fill(seriesIdx, valueIdx, value);
ctx.fill();
if (strokeWidth) {
ctx.beginPath();
rect(ctx, lft + strokeWidth / 2, top + strokeWidth / 2, wid - strokeWidth, hgt - strokeWidth);
ctx.strokeStyle = stroke(seriesIdx, valueIdx, value);
ctx.stroke();
}
}
qt.add({
x: round(lft - xOff),
y: round(top - yOff),
w: wid,
h: hgt,
sidx: seriesIdx + 1,
didx: valueIdx,
});
}
const drawPaths: Series.PathBuilder = (u, sidx, idx0, idx1) => {
uPlot.orient(
u,
sidx,
(series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect) => {
let strokeWidth = round((series.width || 0) * pxRatio);
let discrete = isDiscrete(sidx);
u.ctx.save();
rect(u.ctx, u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
u.ctx.clip();
walk(rowHeight, sidx - 1, numSeries, yDim, (iy, y0, hgt) => {
if (mode === TimelineMode.Spans) {
for (let ix = 0; ix < dataY.length; ix++) {
if (dataY[ix] != null) {
let lft = Math.round(valToPosX(dataX[ix], scaleX, xDim, xOff));
let nextIx = ix;
while (dataY[++nextIx] === undefined && nextIx < dataY.length) {}
// to now (not to end of chart)
let rgt =
nextIx === dataY.length
? xOff + xDim + strokeWidth
: Math.round(valToPosX(dataX[nextIx], scaleX, xDim, xOff));
putBox(
u.ctx,
rect,
xOff,
yOff,
lft,
round(yOff + y0),
rgt - lft,
round(hgt),
strokeWidth,
iy,
ix,
dataY[ix],
discrete
);
ix = nextIx - 1;
}
}
} else if (mode === TimelineMode.Grid) {
let colWid = valToPosX(dataX[1], scaleX, xDim, xOff) - valToPosX(dataX[0], scaleX, xDim, xOff);
let gapWid = colWid * gapFactor;
let barWid = round(min(maxWidth, colWid - gapWid) - strokeWidth);
let xShift = barWid / 2;
//let xShift = align === 1 ? 0 : align === -1 ? barWid : barWid / 2;
for (let ix = idx0; ix <= idx1; ix++) {
if (dataY[ix] != null) {
// TODO: all xPos can be pre-computed once for all series in aligned set
let lft = valToPosX(dataX[ix], scaleX, xDim, xOff);
putBox(
u.ctx,
rect,
xOff,
yOff,
round(lft - xShift),
round(yOff + y0),
barWid,
round(hgt),
strokeWidth,
iy,
ix,
dataY[ix],
discrete
);
}
}
}
});
discrete && drawBoxes(u.ctx);
u.ctx.restore();
}
);
return null;
};
const drawPoints: Series.Points.Show =
formatValue == null || showValue === BarValueVisibility.Never
? false
: (u, sidx, i0, i1) => {
u.ctx.save();
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
u.ctx.clip();
u.ctx.font = font;
u.ctx.fillStyle = 'black';
u.ctx.textAlign = mode === TimelineMode.Spans ? 'left' : 'center';
u.ctx.textBaseline = 'middle';
uPlot.orient(
u,
sidx,
(
series,
dataX,
dataY,
scaleX,
scaleY,
valToPosX,
valToPosY,
xOff,
yOff,
xDim,
yDim,
moveTo,
lineTo,
rect
) => {
let y = round(yOff + yMids[sidx - 1]);
for (let ix = 0; ix < dataY.length; ix++) {
if (dataY[ix] != null) {
let x = valToPosX(dataX[ix], scaleX, xDim, xOff);
u.ctx.fillText(formatValue(sidx, dataY[ix]), x, y);
}
}
}
);
u.ctx.restore();
return false;
};
const init = (u: uPlot) => {
let over = u.root.querySelector('.u-over')! as HTMLElement;
over.style.overflow = 'hidden';
hoverMarks.forEach((m) => {
over.appendChild(m);
});
};
const drawClear = (u: uPlot) => {
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
qt.clear();
// force-clear the path cache to cause drawBars() to rebuild new quadtree
u.series.forEach((s) => {
// @ts-ignore
s._paths = null;
});
};
const setCursor = (u: uPlot) => {
let cx = round(u.cursor!.left! * pxRatio);
for (let i = 0; i < numSeries; i++) {
let found: Rect | null = null;
if (cx >= 0) {
let cy = yMids[i];
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;
}
});
}
let h = hoverMarks[i];
if (found) {
if (found !== hovered[i]) {
hovered[i] = found;
h.style.display = '';
h.style.left = round(found!.x / pxRatio) + 'px';
h.style.top = round(found!.y / pxRatio) + 'px';
h.style.width = round(found!.w / pxRatio) + 'px';
h.style.height = round(found!.h / pxRatio) + 'px';
}
} else if (hovered[i] != null) {
h.style.display = 'none';
hovered[i] = null;
}
}
};
// hide y crosshair & hover points
const cursor: Partial<Cursor> = {
y: false,
points: { show: false },
};
const yMids: number[] = Array(numSeries).fill(0);
const ySplits: number[] = Array(numSeries).fill(0);
return {
cursor,
xSplits:
mode === TimelineMode.Grid
? (u: uPlot, axisIdx: number, scaleMin: number, scaleMax: number, foundIncr: number, foundSpace: number) => {
let splits = [];
let dataIncr = u.data[0][1] - u.data[0][0];
let skipFactor = ceil(foundIncr / dataIncr);
for (let i = 0; i < u.data[0].length; i += skipFactor) {
let v = u.data[0][i];
if (v >= scaleMin && v <= scaleMax) {
splits.push(v);
}
}
return splits;
}
: null,
xRange: (u: uPlot) => {
const r = getTimeRange();
let min = r.from.valueOf();
let max = r.to.valueOf();
if (mode === TimelineMode.Grid) {
let colWid = u.data[0][1] - u.data[0][0];
let scalePad = colWid / 2;
if (min <= u.data[0][0]) {
min = u.data[0][0] - scalePad;
}
let lastIdx = u.data[0].length - 1;
if (max >= u.data[0][lastIdx]) {
max = u.data[0][lastIdx] + scalePad;
}
}
return [min, max] as uPlot.Range.MinMax;
},
ySplits: (u: uPlot) => {
walk(rowHeight, null, numSeries, u.bbox.height, (iy, y0, hgt) => {
// vertical midpoints of each series' timeline (stored relative to .u-over)
yMids[iy] = round(y0 + hgt / 2);
ySplits[iy] = u.posToVal(yMids[iy] / pxRatio, FIXED_UNIT);
});
return ySplits;
},
yValues: (u: uPlot, splits: number[]) => splits.map((v, i) => label(i + 1)),
yRange: [0, 1] as uPlot.Range.MinMax,
// pathbuilders
drawPaths,
drawPoints,
// hooks
init,
drawClear,
setCursor,
};
}

View File

@@ -0,0 +1,59 @@
import { GraphNGProps } from '../GraphNG/GraphNG';
import { GraphGradientMode, HideableFieldConfig } from '../uPlot/config';
import { VizLegendOptions } from '../VizLegend/types';
/**
* @alpha
*/
export enum BarValueVisibility {
Auto = 'auto',
Never = 'never',
Always = 'always',
}
/**
* @alpha
*/
export interface TimelineOptions {
mode: TimelineMode;
legend: VizLegendOptions;
showValue: BarValueVisibility;
rowHeight: number;
colWidth?: number;
}
/**
* @alpha
*/
export interface TimelineFieldConfig extends HideableFieldConfig {
lineWidth?: number; // 0
fillOpacity?: number; // 100
gradientMode?: GraphGradientMode;
}
/**
* @alpha
*/
export const defaultTimelineFieldConfig: TimelineFieldConfig = {
lineWidth: 1,
fillOpacity: 80,
gradientMode: GraphGradientMode.None,
};
/**
* @alpha
*/
export enum TimelineMode {
Spans = 'spans',
Grid = 'grid',
}
/**
* @alpha
*/
export interface TimelineProps extends GraphNGProps {
mode: TimelineMode;
rowHeight: number;
showValue: BarValueVisibility;
colWidth?: number;
}

View File

@@ -0,0 +1,203 @@
import React from 'react';
import { GraphNGLegendEventMode, XYFieldMatchers } from '../GraphNG/types';
import {
DataFrame,
FieldColorModeId,
FieldConfig,
formattedValueToString,
getFieldDisplayName,
GrafanaTheme,
outerJoinDataFrames,
TimeRange,
TimeZone,
classicColors,
Field,
} from '@grafana/data';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { TimelineCoreOptions, getConfig } from './timeline';
import { FIXED_UNIT } from '../GraphNG/GraphNG';
import { AxisPlacement, GraphGradientMode, ScaleDirection, ScaleOrientation } from '../uPlot/config';
import { measureText } from '../../utils/measureText';
import { TimelineFieldConfig } from '../..';
const defaultConfig: TimelineFieldConfig = {
lineWidth: 0,
fillOpacity: 80,
gradientMode: GraphGradientMode.None,
};
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 type uPlotConfigBuilderSupplier = (
frame: DataFrame,
theme: GrafanaTheme,
getTimeRange: () => TimeRange,
getTimeZone: () => TimeZone
) => UPlotConfigBuilder;
export function preparePlotConfigBuilder(
frame: DataFrame,
theme: GrafanaTheme,
getTimeRange: () => TimeRange,
getTimeZone: () => TimeZone,
coreOptions: Partial<TimelineCoreOptions>
): UPlotConfigBuilder {
const builder = new UPlotConfigBuilder(getTimeZone);
const isDiscrete = (field: Field) => {
const mode = field.config?.color?.mode;
return !(mode && field.display && mode.startsWith('continuous-'));
};
const colorLookup = (seriesIdx: number, valueIdx: number, value: any) => {
const field = frame.fields[seriesIdx];
const mode = field.config?.color?.mode;
if (mode && field.display && (mode === FieldColorModeId.Thresholds || mode.startsWith('continuous-'))) {
const disp = field.display(value); // will apply color modes
if (disp.color) {
return disp.color;
}
}
return classicColors[Math.floor(value % classicColors.length)];
};
const yAxisWidth =
frame.fields.reduce((maxWidth, field) => {
return Math.max(
maxWidth,
measureText(getFieldDisplayName(field, frame), Math.round(10 * devicePixelRatio)).width
);
}, 0) + 24;
const opts: TimelineCoreOptions = {
// should expose in panel config
mode: coreOptions.mode!,
numSeries: frame.fields.length - 1,
isDiscrete: (seriesIdx) => isDiscrete(frame.fields[seriesIdx]),
rowHeight: coreOptions.rowHeight!,
colWidth: coreOptions.colWidth,
showValue: coreOptions.showValue!,
label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame),
fill: colorLookup,
stroke: colorLookup,
getTimeRange,
// hardcoded formatter for state values
formatValue: (seriesIdx, value) => formattedValueToString(frame.fields[seriesIdx].display!(value)),
// TODO: unimplemeted for now
onHover: (seriesIdx: number, valueIdx: number) => {
console.log('hover', { seriesIdx, valueIdx });
},
onLeave: (seriesIdx: number, valueIdx: number) => {
console.log('leave', { seriesIdx, valueIdx });
},
};
const coreConfig = getConfig(opts);
builder.addHook('init', coreConfig.init);
builder.addHook('drawClear', coreConfig.drawClear);
builder.addHook('setCursor', coreConfig.setCursor);
builder.setCursor(coreConfig.cursor);
builder.addScale({
scaleKey: 'x',
isTime: true,
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
range: coreConfig.xRange,
});
builder.addScale({
scaleKey: FIXED_UNIT, // y
isTime: false,
orientation: ScaleOrientation.Vertical,
direction: ScaleDirection.Up,
range: coreConfig.yRange,
});
builder.addAxis({
scaleKey: 'x',
isTime: true,
splits: coreConfig.xSplits!,
placement: AxisPlacement.Bottom,
timeZone: getTimeZone(),
theme,
});
builder.addAxis({
scaleKey: FIXED_UNIT, // y
isTime: false,
placement: AxisPlacement.Left,
splits: coreConfig.ySplits,
values: coreConfig.yValues,
grid: false,
ticks: false,
size: yAxisWidth,
gap: 16,
theme,
});
let seriesIndex = 0;
for (let i = 0; i < frame.fields.length; i++) {
if (i === 0) {
continue;
}
const field = frame.fields[i];
const config = field.config as FieldConfig<TimelineFieldConfig>;
const customConfig: TimelineFieldConfig = {
...defaultConfig,
...config.custom,
};
field.state!.seriesIndex = seriesIndex++;
//const scaleKey = config.unit || FIXED_UNIT;
//const colorMode = getFieldColorModeForField(field);
let { fillOpacity } = customConfig;
builder.addSeries({
scaleKey: FIXED_UNIT,
pathBuilder: coreConfig.drawPaths,
pointsBuilder: coreConfig.drawPoints,
//colorMode,
fillOpacity,
theme,
show: !customConfig.hideFrom?.graph,
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;
}

View File

@@ -221,6 +221,8 @@ export { usePlotContext, usePlotPluginContext } from './uPlot/context';
export { GraphNG, FIXED_UNIT } from './GraphNG/GraphNG';
export { useGraphNGContext } from './GraphNG/hooks';
export { BarChart } from './BarChart/BarChart';
export { Timeline } from './Timeline/Timeline';
export { BarChartOptions, BarStackingMode, BarValueVisibility, BarChartFieldConfig } from './BarChart/types';
export { TimelineOptions, TimelineFieldConfig } from './Timeline/types';
export { GraphNGLegendEvent, GraphNGLegendEventMode } from './GraphNG/types';
export * from './NodeGraph';

View File

@@ -2,6 +2,9 @@ import React, { useContext } from 'react';
import uPlot, { AlignedData, Series } from 'uplot';
import { PlotPlugin } from './types';
/**
* @alpha
*/
interface PlotCanvasContextType {
// canvas size css pxs
width: number;
@@ -15,6 +18,9 @@ interface PlotCanvasContextType {
};
}
/**
* @alpha
*/
interface PlotPluginsContextType {
registerPlugin: (plugin: PlotPlugin) => () => void;
}
@@ -28,6 +34,9 @@ interface PlotContextType extends PlotPluginsContextType {
data: AlignedData;
}
/**
* @alpha
*/
export const PlotContext = React.createContext<PlotContextType>({} as PlotContextType);
// Exposes uPlot instance and bounding box of the entire canvas and plot area
@@ -39,7 +48,11 @@ const throwWhenNoContext = (name: string) => {
throw new Error(`${name} must be used within PlotContext or PlotContext is not ready yet!`);
};
// Exposes API for registering uPlot plugins
/**
* Exposes API for registering uPlot plugins
*
* @alpha
*/
export const usePlotPluginContext = (): PlotPluginsContextType => {
const ctx = useContext(PlotContext);
if (Object.keys(ctx).length === 0) {
@@ -50,6 +63,9 @@ export const usePlotPluginContext = (): PlotPluginsContextType => {
};
};
/**
* @alpha
*/
export const buildPlotContext = (
isPlotReady: boolean,
canvasRef: any,