mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: stack by % (#37127)
This commit is contained in:
parent
4fa17c8040
commit
8b80d2256d
@ -4,6 +4,7 @@ import { Vector, QueryResultMeta } from '../types';
|
||||
import { guessFieldTypeFromNameAndValue, toDataFrameDTO } from './processDataFrame';
|
||||
import { FunctionalVector } from '../vector/FunctionalVector';
|
||||
|
||||
/** @public */
|
||||
export type ValueConverter<T = any> = (val: any) => T;
|
||||
|
||||
const NOOP: ValueConverter = (v) => v;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { vectorToArray } from './vectorToArray';
|
||||
import { Vector } from '../types';
|
||||
|
||||
/** @public */
|
||||
export abstract class FunctionalVector<T = any> implements Vector<T>, Iterable<T> {
|
||||
abstract get length(): number;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { AlignedData } from 'uplot';
|
||||
import { Themeable2 } from '../../types';
|
||||
import { findMidPointYPosition, pluginLog, preparePlotData } from '../uPlot/utils';
|
||||
import { findMidPointYPosition, pluginLog } from '../uPlot/utils';
|
||||
import {
|
||||
DataFrame,
|
||||
FieldMatcherID,
|
||||
@ -98,16 +98,20 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
pluginLog('GraphNG', false, 'data aligned', alignedFrame);
|
||||
|
||||
if (alignedFrame) {
|
||||
state = {
|
||||
alignedFrame,
|
||||
alignedData: preparePlotData(alignedFrame),
|
||||
};
|
||||
pluginLog('GraphNG', false, 'data prepared', state.alignedData);
|
||||
let config = this.state?.config;
|
||||
|
||||
if (withConfig) {
|
||||
state.config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange);
|
||||
pluginLog('GraphNG', false, 'config prepared', state.config);
|
||||
config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange);
|
||||
pluginLog('GraphNG', false, 'config prepared', config);
|
||||
}
|
||||
|
||||
state = {
|
||||
alignedFrame,
|
||||
alignedData: config!.prepData!(alignedFrame),
|
||||
config,
|
||||
};
|
||||
|
||||
pluginLog('GraphNG', false, 'data prepared', state.alignedData);
|
||||
}
|
||||
|
||||
return state;
|
||||
@ -123,7 +127,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
.pipe(throttleTime(50))
|
||||
.subscribe({
|
||||
next: (evt) => {
|
||||
const u = this.plotInstance?.current;
|
||||
const u = this.plotInstance.current;
|
||||
if (u) {
|
||||
// Try finding left position on time axis
|
||||
const left = u.valToPos(evt.payload.point.time, 'time');
|
||||
@ -183,6 +187,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
if (shouldReconfig) {
|
||||
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
|
||||
newState.alignedData = newState.config.prepData!(newState.alignedFrame);
|
||||
pluginLog('GraphNG', false, 'config recreated', newState.config);
|
||||
}
|
||||
}
|
||||
|
@ -46,10 +46,9 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
|
||||
super(props);
|
||||
|
||||
const alignedDataFrame = preparePlotFrame(props.sparkline, props.config);
|
||||
const data = preparePlotData(alignedDataFrame);
|
||||
|
||||
this.state = {
|
||||
data,
|
||||
data: preparePlotData(alignedDataFrame),
|
||||
alignedDataFrame,
|
||||
configBuilder: this.prepareConfig(alignedDataFrame),
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
} from '../uPlot/config';
|
||||
import { collectStackingGroups } from '../uPlot/utils';
|
||||
import { collectStackingGroups, preparePlotData } from '../uPlot/utils';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||
@ -46,6 +46,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZone);
|
||||
|
||||
builder.setPrepData(preparePlotData);
|
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = frame.fields[0];
|
||||
if (!xField) {
|
||||
|
@ -285,6 +285,7 @@ export const graphFieldOptions = {
|
||||
stacking: [
|
||||
{ label: 'Off', value: StackingMode.None },
|
||||
{ label: 'Normal', value: StackingMode.Normal },
|
||||
{ label: '100%', value: StackingMode.Percent },
|
||||
] as Array<SelectableValue<StackingMode>>,
|
||||
|
||||
thresholdsDisplayModes: [
|
||||
|
@ -1,4 +1,4 @@
|
||||
import uPlot, { Cursor, Band, Hooks, Select } from 'uplot';
|
||||
import uPlot, { Cursor, Band, Hooks, Select, AlignedData } from 'uplot';
|
||||
import { merge } from 'lodash';
|
||||
import {
|
||||
DataFrame,
|
||||
@ -35,6 +35,8 @@ const cursorDefaults: Cursor = {
|
||||
},
|
||||
};
|
||||
|
||||
type PrepData = (frame: DataFrame) => AlignedData;
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
private series: UPlotSeriesBuilder[] = [];
|
||||
private axes: Record<string, UPlotAxisBuilder> = {};
|
||||
@ -56,6 +58,8 @@ export class UPlotConfigBuilder {
|
||||
*/
|
||||
tooltipInterpolator: PlotTooltipInterpolator | undefined = undefined;
|
||||
|
||||
prepData: PrepData | undefined = undefined;
|
||||
|
||||
constructor(timeZone: TimeZone = DefaultTimeZone) {
|
||||
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
|
||||
}
|
||||
@ -153,6 +157,10 @@ export class UPlotConfigBuilder {
|
||||
this.tooltipInterpolator = interpolator;
|
||||
}
|
||||
|
||||
setPrepData(prepData: PrepData) {
|
||||
this.prepData = prepData;
|
||||
}
|
||||
|
||||
setSync() {
|
||||
this.sync = true;
|
||||
}
|
||||
|
@ -34,7 +34,12 @@ export const DEFAULT_PLOT_CONFIG: Partial<Options> = {
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotData(frame: DataFrame): AlignedData {
|
||||
interface StackMeta {
|
||||
totals: AlignedData;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotData(frame: DataFrame, onStackMeta?: (meta: StackMeta) => void): AlignedData {
|
||||
const result: any[] = [];
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
let seriesIndex = 0;
|
||||
@ -64,21 +69,48 @@ export function preparePlotData(frame: DataFrame): AlignedData {
|
||||
|
||||
// Stacking
|
||||
if (stackingGroups.size !== 0) {
|
||||
const byPct = frame.fields[1].config.custom?.stacking?.mode === StackingMode.Percent;
|
||||
const dataLength = result[0].length;
|
||||
const alignedTotals = Array(stackingGroups.size);
|
||||
alignedTotals[0] = null;
|
||||
|
||||
// array or stacking groups
|
||||
for (const [_, seriesIdxs] of stackingGroups.entries()) {
|
||||
const acc = Array(result[0].length).fill(0);
|
||||
const groupTotals = byPct ? Array(dataLength).fill(0) : null;
|
||||
|
||||
if (byPct) {
|
||||
for (let j = 0; j < seriesIdxs.length; j++) {
|
||||
const currentlyStacking = result[seriesIdxs[j]];
|
||||
|
||||
for (let k = 0; k < dataLength; k++) {
|
||||
const v = currentlyStacking[k];
|
||||
groupTotals![k] += v == null ? 0 : +v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const acc = Array(dataLength).fill(0);
|
||||
|
||||
for (let j = 0; j < seriesIdxs.length; j++) {
|
||||
const currentlyStacking = result[seriesIdxs[j]];
|
||||
let seriesIdx = seriesIdxs[j];
|
||||
|
||||
for (let k = 0; k < result[0].length; k++) {
|
||||
alignedTotals[seriesIdx] = groupTotals;
|
||||
|
||||
const currentlyStacking = result[seriesIdx];
|
||||
|
||||
for (let k = 0; k < dataLength; k++) {
|
||||
const v = currentlyStacking[k];
|
||||
acc[k] += v == null ? 0 : +v;
|
||||
acc[k] += v == null ? 0 : v / (byPct ? groupTotals![k] : 1);
|
||||
}
|
||||
|
||||
result[seriesIdxs[j]] = acc.slice();
|
||||
result[seriesIdx] = acc.slice();
|
||||
}
|
||||
}
|
||||
|
||||
onStackMeta &&
|
||||
onStackMeta({
|
||||
totals: alignedTotals as AlignedData,
|
||||
});
|
||||
}
|
||||
|
||||
return result as AlignedData;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { PanelProps, TimeRange, VizOrientation } from '@grafana/data';
|
||||
import { StackingMode, TooltipDisplayMode, TooltipPlugin } from '@grafana/ui';
|
||||
import { StackingMode, TooltipDisplayMode, TooltipPlugin, useTheme2 } from '@grafana/ui';
|
||||
import { BarChartOptions } from './types';
|
||||
import { BarChart } from './BarChart';
|
||||
import { prepareGraphableFrames } from './utils';
|
||||
@ -11,8 +11,11 @@ interface Props extends PanelProps<BarChartOptions> {}
|
||||
* @alpha
|
||||
*/
|
||||
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone }) => {
|
||||
const { frames, warn } = useMemo(() => prepareGraphableFrames(data?.series, options.stacking), [
|
||||
const theme = useTheme2();
|
||||
|
||||
const { frames, warn } = useMemo(() => prepareGraphableFrames(data?.series, theme, options.stacking), [
|
||||
data,
|
||||
theme,
|
||||
options.stacking,
|
||||
]);
|
||||
const orientation = useMemo(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import uPlot, { Axis } from 'uplot';
|
||||
import uPlot, { Axis, AlignedData } from 'uplot';
|
||||
import { pointWithin, Quadtree, Rect } from './quadtree';
|
||||
import { distribute, SPACE_BETWEEN } from './distribute';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
calculateFontSize,
|
||||
PlotTooltipInterpolator,
|
||||
@ -11,6 +11,7 @@ import {
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
} from '@grafana/ui';
|
||||
import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
|
||||
const groupDistr = SPACE_BETWEEN;
|
||||
const barDistr = SPACE_BETWEEN;
|
||||
@ -52,10 +53,17 @@ export interface BarsOptions {
|
||||
* @internal
|
||||
*/
|
||||
export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
const { xOri, xDir: dir, groupWidth, barWidth, rawValue, formatValue, showValue } = opts;
|
||||
const { xOri, xDir: dir, rawValue, formatValue, showValue } = opts;
|
||||
const isXHorizontal = xOri === ScaleOrientation.Horizontal;
|
||||
const hasAutoValueSize = !Boolean(opts.text?.valueSize);
|
||||
const isStacked = opts.stacking !== StackingMode.None;
|
||||
const pctStacked = opts.stacking === StackingMode.Percent;
|
||||
|
||||
let { groupWidth, barWidth } = opts;
|
||||
|
||||
if (isStacked) {
|
||||
[groupWidth, barWidth] = [barWidth, groupWidth];
|
||||
}
|
||||
|
||||
let qt: Quadtree;
|
||||
let hovered: Rect | undefined = undefined;
|
||||
@ -200,7 +208,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
let labelOffset = LABEL_OFFSET_MAX;
|
||||
|
||||
barRects.forEach((r, i) => {
|
||||
texts[i] = formatValue(r.sidx, rawValue(r.sidx, r.didx));
|
||||
texts[i] = formatValue(r.sidx, rawValue(r.sidx, r.didx)! / (pctStacked ? alignedTotals![r.sidx][r.didx]! : 1));
|
||||
labelOffset = Math.min(labelOffset, Math.round(LABEL_OFFSET_FACTOR * (isXHorizontal ? r.w : r.h)));
|
||||
});
|
||||
|
||||
@ -302,6 +310,16 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
};
|
||||
};
|
||||
|
||||
let alignedTotals: AlignedData | null = null;
|
||||
|
||||
function prepData(alignedFrame: DataFrame) {
|
||||
alignedTotals = null;
|
||||
|
||||
return preparePlotData(alignedFrame, ({ totals }) => {
|
||||
alignedTotals = totals;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cursor: {
|
||||
x: false,
|
||||
@ -318,5 +336,6 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
drawClear,
|
||||
draw,
|
||||
interpolateTooltip,
|
||||
prepData,
|
||||
};
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ describe('BarChart utils', () => {
|
||||
|
||||
describe('prepareGraphableFrames', () => {
|
||||
it('will warn when there is no data in the response', () => {
|
||||
const result = prepareGraphableFrames([], StackingMode.None);
|
||||
const result = prepareGraphableFrames([], createTheme(), StackingMode.None);
|
||||
expect(result.warn).toEqual('No data in response');
|
||||
});
|
||||
|
||||
@ -154,7 +154,7 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', values: [1, 2, 3, 4, 5] },
|
||||
],
|
||||
});
|
||||
const result = prepareGraphableFrames([df], StackingMode.None);
|
||||
const result = prepareGraphableFrames([df], createTheme(), StackingMode.None);
|
||||
expect(result.warn).toEqual('Bar charts requires a string field');
|
||||
expect(result.frames).toBeUndefined();
|
||||
});
|
||||
@ -166,7 +166,7 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', type: FieldType.boolean, values: [true, true, true, true, true] },
|
||||
],
|
||||
});
|
||||
const result = prepareGraphableFrames([df], StackingMode.None);
|
||||
const result = prepareGraphableFrames([df], createTheme(), StackingMode.None);
|
||||
expect(result.warn).toEqual('No numeric fields found');
|
||||
expect(result.frames).toBeUndefined();
|
||||
});
|
||||
@ -178,7 +178,7 @@ describe('BarChart utils', () => {
|
||||
{ name: 'value', values: [-10, NaN, 10, -Infinity, +Infinity] },
|
||||
],
|
||||
});
|
||||
const result = prepareGraphableFrames([df], StackingMode.None);
|
||||
const result = prepareGraphableFrames([df], createTheme(), StackingMode.None);
|
||||
|
||||
const field = result.frames![0].fields[1];
|
||||
expect(field!.values.toArray()).toMatchInlineSnapshot(`
|
||||
|
@ -4,8 +4,10 @@ import {
|
||||
Field,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getDisplayProcessor,
|
||||
getFieldColorModeForField,
|
||||
getFieldSeriesColor,
|
||||
GrafanaTheme2,
|
||||
MutableDataFrame,
|
||||
VizOrientation,
|
||||
} from '@grafana/data';
|
||||
@ -89,6 +91,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
|
||||
builder.setTooltipInterpolator(config.interpolateTooltip);
|
||||
|
||||
builder.setPrepData(config.prepData);
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
isTime: false,
|
||||
@ -222,6 +226,7 @@ export function preparePlotFrame(data: DataFrame[]) {
|
||||
/** @internal */
|
||||
export function prepareGraphableFrames(
|
||||
series: DataFrame[],
|
||||
theme: GrafanaTheme2,
|
||||
stacking: StackingMode
|
||||
): { frames?: DataFrame[]; warn?: string } {
|
||||
if (!series?.length) {
|
||||
@ -268,6 +273,12 @@ export function prepareGraphableFrames(
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
if (stacking === StackingMode.Percent) {
|
||||
copy.config.unit = 'percentunit';
|
||||
copy.display = getDisplayProcessor({ field: copy, theme });
|
||||
}
|
||||
|
||||
fields.push(copy);
|
||||
} else {
|
||||
fields.push({ ...field });
|
||||
|
@ -27,6 +27,7 @@ import { getConfig, TimelineCoreOptions } from './timeline';
|
||||
import { AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/ui/src/components/uPlot/config';
|
||||
import { TimelineFieldConfig, TimelineOptions } from './types';
|
||||
import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types';
|
||||
import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
|
||||
const defaultConfig: TimelineFieldConfig = {
|
||||
lineWidth: 0,
|
||||
@ -140,6 +141,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
|
||||
|
||||
builder.setTooltipInterpolator(interpolateTooltip);
|
||||
|
||||
builder.setPrepData(preparePlotData);
|
||||
|
||||
builder.setCursor(coreConfig.cursor);
|
||||
|
||||
builder.addScale({
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
GrafanaTheme2,
|
||||
isBooleanUnit,
|
||||
} from '@grafana/data';
|
||||
import { GraphFieldConfig, LineInterpolation } from '@grafana/ui';
|
||||
import { GraphFieldConfig, LineInterpolation, StackingMode } from '@grafana/ui';
|
||||
|
||||
// This will return a set of frames with only graphable values included
|
||||
export function prepareGraphableFields(
|
||||
@ -46,6 +46,12 @@ export function prepareGraphableFields(
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
if (copy.config.custom?.stacking?.mode === StackingMode.Percent) {
|
||||
copy.config.unit = 'percentunit';
|
||||
copy.display = getDisplayProcessor({ field: copy, theme });
|
||||
}
|
||||
|
||||
fields.push(copy);
|
||||
break; // ok
|
||||
case FieldType.boolean:
|
||||
|
Loading…
Reference in New Issue
Block a user