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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user