GraphNG: stack by % (#37127)

This commit is contained in:
Leon Sorokin
2021-07-28 20:31:07 -05:00
committed by GitHub
parent 4fa17c8040
commit 8b80d2256d
14 changed files with 121 additions and 30 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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),
};

View File

@@ -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) {

View File

@@ -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: [

View File

@@ -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;
}

View File

@@ -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;