From 917b5c5f2a1094987c33bda72abc25989f7af5b8 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 20 Nov 2020 23:49:36 -0600 Subject: [PATCH] [graph-ng] add temporal DataFrame alignment/outerJoin & move null-asZero pass inside (#29250) [GraphNG] update uPlot, add temporal DataFrame alignment/outerJoin, move null-asZero pass inside, merge isGap updates into u.setData() calls. Co-authored-by: Ryan McKinley --- packages/grafana-ui/package.json | 2 +- .../src/components/GraphNG/GraphNG.tsx | 21 ++- .../src/components/GraphNG/utils.ts | 172 +++++++++++++++--- .../grafana-ui/src/components/uPlot/Plot.tsx | 20 +- .../uPlot/config/UPlotAxisBuilder.ts | 12 +- .../uPlot/config/UPlotConfigBuilder.test.ts | 3 +- .../uPlot/config/UPlotScaleBuilder.ts | 4 +- .../uPlot/config/UPlotSeriesBuilder.ts | 4 +- .../src/components/uPlot/context.ts | 16 +- .../grafana-ui/src/components/uPlot/hooks.ts | 4 +- .../src/components/uPlot/renderPlugin.ts | 76 -------- .../grafana-ui/src/components/uPlot/types.ts | 20 +- .../grafana-ui/src/components/uPlot/utils.ts | 43 +---- yarn.lock | 8 +- 14 files changed, 211 insertions(+), 194 deletions(-) mode change 100644 => 100755 packages/grafana-ui/src/components/GraphNG/GraphNG.tsx mode change 100644 => 100755 packages/grafana-ui/src/components/GraphNG/utils.ts mode change 100644 => 100755 packages/grafana-ui/src/components/uPlot/Plot.tsx mode change 100644 => 100755 packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts delete mode 100644 packages/grafana-ui/src/components/uPlot/renderPlugin.ts mode change 100644 => 100755 packages/grafana-ui/src/components/uPlot/types.ts mode change 100644 => 100755 packages/grafana-ui/src/components/uPlot/utils.ts diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index b10ea7e0aad..6e1590983d8 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -71,7 +71,7 @@ "react-transition-group": "4.4.1", "slate": "0.47.8", "tinycolor2": "1.4.1", - "uplot": "1.3.0" + "uplot": "1.4.4" }, "devDependencies": { "@rollup/plugin-commonjs": "11.0.2", diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx old mode 100644 new mode 100755 index ad159dfcdea..b2ece8e9b69 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -7,9 +7,8 @@ import { getFieldColorModeForField, getFieldDisplayName, getTimeField, - TIME_SERIES_TIME_FIELD_NAME, } from '@grafana/data'; -import { alignAndSortDataFramesByFieldName } from './utils'; +import { mergeTimeSeriesData } from './utils'; import { UPlotChart } from '../uPlot/Plot'; import { PlotProps } from '../uPlot/types'; import { AxisPlacement, getUPlotSideFromAxis, GraphFieldConfig, GraphMode, PointMode } from '../uPlot/config'; @@ -43,11 +42,11 @@ export const GraphNG: React.FC = ({ ...plotProps }) => { const theme = useTheme(); - const alignedData = useMemo(() => alignAndSortDataFramesByFieldName(data, TIME_SERIES_TIME_FIELD_NAME), [data]); + const alignedFrameWithGapTest = useMemo(() => mergeTimeSeriesData(data), [data]); const legendItemsRef = useRef([]); const hasLegend = legend && legend.displayMode !== LegendDisplayMode.Hidden; - if (!alignedData) { + if (alignedFrameWithGapTest == null) { return (

No data found in response

@@ -55,10 +54,12 @@ export const GraphNG: React.FC = ({ ); } + const alignedFrame = alignedFrameWithGapTest.frame; + const configBuilder = useMemo(() => { const builder = new UPlotConfigBuilder(); - let { timeIndex } = getTimeField(alignedData); + let { timeIndex } = getTimeField(alignedFrame); if (timeIndex === undefined) { timeIndex = 0; // assuming first field represents x-domain @@ -85,8 +86,8 @@ export const GraphNG: React.FC = ({ let hasLeftAxis = false; let hasYAxis = false; - for (let i = 0; i < alignedData.fields.length; i++) { - const field = alignedData.fields[i]; + for (let i = 0; i < alignedFrame.fields.length; i++) { + const field = alignedFrame.fields[i]; const config = field.config as FieldConfig; const customConfig = config.custom || defaultConfig; @@ -137,7 +138,7 @@ export const GraphNG: React.FC = ({ if (hasLegend) { legendItems.push({ color: seriesColor, - label: getFieldDisplayName(field, alignedData), + label: getFieldDisplayName(field, alignedFrame), yAxis: side === AxisPlacement.Right ? 3 : 1, }); } @@ -147,7 +148,7 @@ export const GraphNG: React.FC = ({ legendItemsRef.current = legendItems; return builder; - }, [alignedData, hasLegend]); + }, [alignedFrameWithGapTest, hasLegend]); let legendElement: React.ReactElement | undefined; @@ -163,7 +164,7 @@ export const GraphNG: React.FC = ({ {(vizWidth: number, vizHeight: number) => ( { - if (!data.length) { - return null; - } +/** + * Returns a single DataFrame with: + * - A shared time column + * - only numeric fields + * + * The input expects all frames to have a time field with values in ascending order + * + * @alpha + */ +export function mergeTimeSeriesData(frames: DataFrame[]): AlignedFrameWithGapTest | null { + const valuesFromFrames: AlignedData[] = []; + const sourceFields: Field[] = []; - // normalize time field names - // in each frame find first time field and rename it to unified name - for (let i = 0; i < data.length; i++) { - const series = data[i]; - for (let j = 0; j < series.fields.length; j++) { - const field = series.fields[j]; - if (field.type === FieldType.time) { - field.name = fieldName; - break; + for (const frame of frames) { + const { timeField } = getTimeField(frame); + if (!timeField) { + continue; + } + + const alignedData: AlignedData = [ + timeField.values.toArray(), // The x axis (time) + ]; + + // find numeric fields + for (const field of frame.fields) { + if (field.type !== FieldType.number) { + continue; } + + let values = field.values.toArray(); + if (field.config.nullValueMode === NullValueMode.AsZero) { + values = values.map(v => (v === null ? 0 : v)); + } + alignedData.push(values); + + // Add the first time field + if (sourceFields.length < 1) { + sourceFields.push(timeField); + } + + // This will cache an appropriate field name in the field state + getFieldDisplayName(field, frame, frames); + sourceFields.push(field); + } + + // Timeseries has tima and at least one number + if (alignedData.length > 1) { + valuesFromFrames.push(alignedData); } } - const dataFramesToPlot = data.filter(frame => { - let { timeIndex } = getTimeField(frame); - // filter out series without time index or if the time column is the only one (i.e. after transformations) - // won't live long as we gona move out from assuming x === time - return timeIndex !== undefined ? frame.fields.length > 1 : false; - }); + if (valuesFromFrames.length === 0) { + return null; + } - const aligned = outerJoinDataFrames(dataFramesToPlot, { byField: fieldName })[0]; - return sortDataFrame(aligned, getTimeField(aligned).timeIndex!); -}; + // do the actual alignment (outerJoin on the first arrays) + const { data: alignedData, isGap } = outerJoinValues(valuesFromFrames); + + if (alignedData!.length !== sourceFields.length) { + throw new Error('outerJoinValues lost a field?'); + } + + // Replace the values from the outer-join field + return { + frame: { + length: alignedData![0].length, + fields: alignedData!.map((vals, idx) => ({ + ...sourceFields[idx], + values: new ArrayVector(vals), + })), + }, + isGap, + }; +} + +export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest { + if (tables.length === 1) { + return { + data: tables[0], + isGap: () => true, + }; + } + + let xVals: Set = new Set(); + let xNulls: Array> = [new Set()]; + + for (const t of tables) { + let xs = t[0]; + let len = xs.length; + let nulls: Set = new Set(); + + for (let i = 0; i < len; i++) { + xVals.add(xs[i]); + } + + for (let j = 1; j < t.length; j++) { + let ys = t[j]; + + for (let i = 0; i < len; i++) { + if (ys[i] == null) { + nulls.add(xs[i]); + } + } + } + + xNulls.push(nulls); + } + + let data: AlignedData = [Array.from(xVals).sort((a, b) => a - b)]; + + let alignedLen = data[0].length; + + let xIdxs = new Map(); + + for (let i = 0; i < alignedLen; i++) { + xIdxs.set(data[0][i], i); + } + + for (const t of tables) { + let xs = t[0]; + + for (let j = 1; j < t.length; j++) { + let ys = t[j]; + + let yVals = Array(alignedLen).fill(null); + + for (let i = 0; i < ys.length; i++) { + yVals[xIdxs.get(xs[i])] = ys[i]; + } + + data.push(yVals); + } + } + + return { + data: data, + isGap(u: uPlot, seriesIdx: number, dataIdx: number) { + // u.data has to be AlignedDate + let xVal = u.data[0][dataIdx]; + return xNulls[seriesIdx].has(xVal!); + }, + }; +} diff --git a/packages/grafana-ui/src/components/uPlot/Plot.tsx b/packages/grafana-ui/src/components/uPlot/Plot.tsx old mode 100644 new mode 100755 index dac66ec76b2..48b9c0ef2f0 --- a/packages/grafana-ui/src/components/uPlot/Plot.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { usePrevious } from 'react-use'; -import uPlot from 'uplot'; +import uPlot, { Options, AlignedData, AlignedDataWithGapTest } from 'uplot'; import { buildPlotContext, PlotContext } from './context'; -import { pluginLog, preparePlotData, shouldInitialisePlot } from './utils'; +import { pluginLog, shouldInitialisePlot } from './utils'; import { usePlotConfig } from './hooks'; import { PlotProps } from './types'; @@ -12,7 +12,7 @@ import { PlotProps } from './types'; export const UPlotChart: React.FC = props => { const canvasRef = useRef(null); const [plotInstance, setPlotInstance] = useState(); - const plotData = useRef(); + const plotData = useRef(); // uPlot config API const { currentConfig, registerPlugin } = usePlotConfig(props.width, props.height, props.timeZone, props.config); @@ -33,11 +33,12 @@ export const UPlotChart: React.FC = props => { return; } pluginLog('uPlot core', false, 'updating plot data(throttled log!)', plotData.current); + // If config hasn't changed just update uPlot's data plotInstance.setData(plotData.current); if (props.onDataUpdate) { - props.onDataUpdate(plotData.current); + props.onDataUpdate(plotData.current.data!); } }, [plotInstance, props.onDataUpdate]); @@ -50,7 +51,10 @@ export const UPlotChart: React.FC = props => { }, [plotInstance]); useLayoutEffect(() => { - plotData.current = preparePlotData(props.data); + plotData.current = { + data: props.data.frame.fields.map(f => f.values.toArray()) as AlignedData, + isGap: props.data.isGap, + }; }, [props.data]); // Decides if plot should update data or re-initialise @@ -62,7 +66,7 @@ export const UPlotChart: React.FC = props => { // Do nothing if there is data vs series config mismatch. This may happen when the data was updated and made this // effect fire before the config update triggered the effect. - if (currentConfig.series.length !== plotData.current.length) { + if (currentConfig.series.length !== plotData.current.data!.length) { return; } @@ -70,7 +74,7 @@ export const UPlotChart: React.FC = props => { if (!canvasRef.current) { throw new Error('Missing Canvas component as a child of the plot.'); } - const instance = initPlot(plotData.current, currentConfig, canvasRef.current); + const instance = initPlot(plotData.current.data!, currentConfig, canvasRef.current); if (props.onPlotInit) { props.onPlotInit(); @@ -106,7 +110,7 @@ export const UPlotChart: React.FC = props => { }; // Main function initialising uPlot. If final config is not settled it will do nothing -function initPlot(data: uPlot.AlignedData, config: uPlot.Options, ref: HTMLDivElement) { +function initPlot(data: AlignedData, config: Options, ref: HTMLDivElement) { pluginLog('uPlot core', false, 'initialized with', data, config); return new uPlot(config, data, ref); } diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index db52050ada9..d661a8c4390 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -1,6 +1,6 @@ import { dateTimeFormat, GrafanaTheme, systemDateFormats, TimeZone } from '@grafana/data'; -import uPlot from 'uplot'; -import { AxisSide, PlotConfigBuilder } from '../types'; +import uPlot, { Axis } from 'uplot'; +import { PlotConfigBuilder } from '../types'; import { measureText } from '../../../utils/measureText'; export interface AxisProps { @@ -10,7 +10,7 @@ export interface AxisProps { stroke?: string; show?: boolean; size?: number; - side?: AxisSide; + side?: Axis.Side; grid?: boolean; formatValue?: (v: any) => string; values?: any; @@ -18,8 +18,8 @@ export interface AxisProps { timeZone?: TimeZone; } -export class UPlotAxisBuilder extends PlotConfigBuilder { - getConfig(): uPlot.Axis { +export class UPlotAxisBuilder extends PlotConfigBuilder { + getConfig(): Axis { const { scaleKey, label, @@ -35,7 +35,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { const stroke = this.props.stroke || theme.colors.text; const gridColor = theme.isDark ? theme.palette.gray25 : theme.palette.gray90; - let config: uPlot.Axis = { + let config: Axis = { scale: scaleKey, label, show, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index ff9f9b751cc..782e1d9d9e9 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -1,7 +1,6 @@ // TODO: migrate tests below to the builder import { UPlotConfigBuilder } from './UPlotConfigBuilder'; -import { AxisSide } from '../types'; import { GrafanaTheme } from '@grafana/data'; import { expect } from '../../../../../../public/test/lib/common'; @@ -56,7 +55,7 @@ describe('UPlotConfigBuilder', () => { scaleKey: 'scale-x', label: 'test label', timeZone: 'browser', - side: AxisSide.Bottom, + side: 2, isTime: false, formatValue: () => 'test value', grid: false, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts index ca421d2889c..2eaa2600446 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts @@ -1,4 +1,4 @@ -import uPlot from 'uplot'; +import { Scale } from 'uplot'; import { PlotConfigBuilder } from '../types'; export interface ScaleProps { @@ -6,7 +6,7 @@ export interface ScaleProps { isTime?: boolean; } -export class UPlotScaleBuilder extends PlotConfigBuilder { +export class UPlotScaleBuilder extends PlotConfigBuilder { getConfig() { const { isTime, scaleKey } = this.props; return { diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts old mode 100644 new mode 100755 index 05b3d938bd3..10c7ef2d8da --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -1,5 +1,5 @@ import tinycolor from 'tinycolor2'; -import uPlot from 'uplot'; +import { Series } from 'uplot'; import { PlotConfigBuilder } from '../types'; export interface SeriesProps { @@ -15,7 +15,7 @@ export interface SeriesProps { fillColor?: string; } -export class UPlotSeriesBuilder extends PlotConfigBuilder { +export class UPlotSeriesBuilder extends PlotConfigBuilder { getConfig() { const { line, lineColor, lineWidth, points, pointColor, pointSize, fillColor, fillOpacity, scaleKey } = this.props; diff --git a/packages/grafana-ui/src/components/uPlot/context.ts b/packages/grafana-ui/src/components/uPlot/context.ts index b9877e40b3d..cc6d4806b4f 100644 --- a/packages/grafana-ui/src/components/uPlot/context.ts +++ b/packages/grafana-ui/src/components/uPlot/context.ts @@ -1,6 +1,6 @@ import React, { useCallback, useContext } from 'react'; -import uPlot from 'uplot'; -import { PlotPlugin } from './types'; +import uPlot, { Series } from 'uplot'; +import { PlotPlugin, AlignedFrameWithGapTest } from './types'; import { DataFrame, Field, FieldConfig } from '@grafana/data'; interface PlotCanvasContextType { @@ -23,10 +23,10 @@ interface PlotPluginsContextType { interface PlotContextType extends PlotPluginsContextType { isPlotReady: boolean; getPlotInstance: () => uPlot; - getSeries: () => uPlot.Series[]; + getSeries: () => Series[]; getCanvas: () => PlotCanvasContextType; canvasRef: any; - data: DataFrame; + data: AlignedFrameWithGapTest; } export const PlotContext = React.createContext({} as PlotContextType); @@ -76,7 +76,7 @@ export const usePlotData = (): PlotDataAPI => { if (!ctx) { throwWhenNoContext('usePlotData'); } - return ctx!.data.fields[idx]; + return ctx!.data.frame.fields[idx]; }, [ctx] ); @@ -109,7 +109,7 @@ export const usePlotData = (): PlotDataAPI => { } // by uPlot convention x-axis is always first field // this may change when we introduce non-time x-axis and multiple x-axes (https://leeoniya.github.io/uPlot/demos/time-periods.html) - return ctx!.data.fields.slice(1); + return ctx!.data.frame.fields.slice(1); }, [ctx]); if (!ctx) { @@ -117,7 +117,7 @@ export const usePlotData = (): PlotDataAPI => { } return { - data: ctx.data, + data: ctx.data.frame, getField, getFieldValue, getFieldConfig, @@ -129,7 +129,7 @@ export const usePlotData = (): PlotDataAPI => { export const buildPlotContext = ( isPlotReady: boolean, canvasRef: any, - data: DataFrame, + data: AlignedFrameWithGapTest, registerPlugin: any, getPlotInstance: () => uPlot ): PlotContextType => { diff --git a/packages/grafana-ui/src/components/uPlot/hooks.ts b/packages/grafana-ui/src/components/uPlot/hooks.ts index 8aacad8f167..0568f5ffbfb 100644 --- a/packages/grafana-ui/src/components/uPlot/hooks.ts +++ b/packages/grafana-ui/src/components/uPlot/hooks.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PlotPlugin } from './types'; import { pluginLog } from './utils'; -import uPlot from 'uplot'; +import uPlot, { Options } from 'uplot'; import { getTimeZoneInfo, TimeZone } from '@grafana/data'; import { usePlotPluginContext } from './context'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; @@ -106,7 +106,7 @@ export const DEFAULT_PLOT_CONFIG = { export const usePlotConfig = (width: number, height: number, timeZone: TimeZone, configBuilder: UPlotConfigBuilder) => { const { arePluginsReady, plugins, registerPlugin } = usePlotPlugins(); - const [currentConfig, setCurrentConfig] = useState(); + const [currentConfig, setCurrentConfig] = useState(); const tzDate = useMemo(() => { let fmt = undefined; diff --git a/packages/grafana-ui/src/components/uPlot/renderPlugin.ts b/packages/grafana-ui/src/components/uPlot/renderPlugin.ts deleted file mode 100644 index 9041dde21c1..00000000000 --- a/packages/grafana-ui/src/components/uPlot/renderPlugin.ts +++ /dev/null @@ -1,76 +0,0 @@ -import uPlot from 'uplot'; - -export function renderPlugin({ spikes = 4, outerRadius = 8, innerRadius = 4 } = {}) { - outerRadius *= devicePixelRatio; - innerRadius *= devicePixelRatio; - - // https://stackoverflow.com/questions/25837158/how-to-draw-a-star-by-using-canvas-html5 - function drawStar(ctx: any, cx: number, cy: number) { - let rot = (Math.PI / 2) * 3; - let x = cx; - let y = cy; - let step = Math.PI / spikes; - - ctx.beginPath(); - ctx.moveTo(cx, cy - outerRadius); - - for (let i = 0; i < spikes; i++) { - x = cx + Math.cos(rot) * outerRadius; - y = cy + Math.sin(rot) * outerRadius; - ctx.lineTo(x, y); - rot += step; - - x = cx + Math.cos(rot) * innerRadius; - y = cy + Math.sin(rot) * innerRadius; - ctx.lineTo(x, y); - rot += step; - } - - ctx.lineTo(cx, cy - outerRadius); - ctx.closePath(); - } - - function drawPointsAsStars(u: uPlot, i: number, i0: any, i1: any) { - let { ctx } = u; - let { stroke, scale } = u.series[i]; - - ctx.fillStyle = stroke as string; - - let j = i0; - - while (j <= i1) { - const val = u.data[i][j] as number; - const cx = Math.round(u.valToPos(u.data[0][j] as number, 'x', true)); - const cy = Math.round(u.valToPos(val, scale as string, true)); - - drawStar(ctx, cx, cy); - ctx.fill(); - - // const zy = Math.round(u.valToPos(0, scale as string, true)); - - // ctx.beginPath(); - // ctx.lineWidth = 3; - // ctx.moveTo(cx, cy - outerRadius); - // ctx.lineTo(cx, zy); - // ctx.stroke(); - // ctx.fill(); - - j++; - } - } - - return { - opts: (u: uPlot, opts: uPlot.Options) => { - opts.series.forEach((s, i) => { - if (i > 0) { - uPlot.assign(s, { - points: { - show: drawPointsAsStars, - }, - }); - } - }); - }, - hooks: {}, // can add callbacks here - }; -} diff --git a/packages/grafana-ui/src/components/uPlot/types.ts b/packages/grafana-ui/src/components/uPlot/types.ts old mode 100644 new mode 100755 index 3ffc16ed49c..a0224104184 --- a/packages/grafana-ui/src/components/uPlot/types.ts +++ b/packages/grafana-ui/src/components/uPlot/types.ts @@ -1,14 +1,14 @@ import React from 'react'; -import uPlot from 'uplot'; +import uPlot, { Options, AlignedData, Series, Hooks } from 'uplot'; import { DataFrame, TimeRange, TimeZone } from '@grafana/data'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; -export type PlotSeriesConfig = Pick; +export type PlotSeriesConfig = Pick; export type PlotPlugin = { id: string; /** can mutate provided opts as necessary */ - opts?: (self: uPlot, opts: uPlot.Options) => void; - hooks: uPlot.PluginHooks; + opts?: (self: uPlot, opts: Options) => void; + hooks: Hooks.ArraysOrFuncs; }; export interface PlotPluginProps { @@ -16,7 +16,7 @@ export interface PlotPluginProps { } export interface PlotProps { - data: DataFrame; + data: AlignedFrameWithGapTest; timeRange: TimeRange; timeZone: TimeZone; width: number; @@ -24,7 +24,7 @@ export interface PlotProps { config: UPlotConfigBuilder; children?: React.ReactElement[]; /** Callback performed when uPlot data is updated */ - onDataUpdate?: (data: uPlot.AlignedData) => {}; + onDataUpdate?: (data: AlignedData) => {}; /** Callback performed when uPlot is (re)initialized */ onPlotInit?: () => {}; } @@ -34,9 +34,7 @@ export abstract class PlotConfigBuilder { abstract getConfig(): T; } -export enum AxisSide { - Top, // 0 - Right, // 1 - Bottom, // 2 - Left, // 3 +export interface AlignedFrameWithGapTest { + frame: DataFrame; + isGap: Series.isGap; } diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts old mode 100644 new mode 100755 index 0cf9141e389..0f1c9ff8a81 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -1,8 +1,8 @@ import throttle from 'lodash/throttle'; import isEqual from 'lodash/isEqual'; import omit from 'lodash/omit'; -import { DataFrame, FieldType, getTimeField, rangeUtil, RawTimeRange } from '@grafana/data'; -import uPlot from 'uplot'; +import { rangeUtil, RawTimeRange } from '@grafana/data'; +import { Options } from 'uplot'; import { PlotPlugin, PlotProps } from './types'; const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g; @@ -16,7 +16,7 @@ export function rangeToMinMax(timeRange: RawTimeRange): [number, number] { return [v.from.valueOf() / 1000, v.to.valueOf() / 1000]; } -export const buildPlotConfig = (props: PlotProps, plugins: Record): uPlot.Options => { +export const buildPlotConfig = (props: PlotProps, plugins: Record): Options => { return { width: props.width, height: props.height, @@ -38,40 +38,7 @@ export const buildPlotConfig = (props: PlotProps, plugins: Record { - const plotData: any[] = []; - - // Prepare x axis - let { timeIndex } = getTimeField(data); - let xvals = data.fields[timeIndex!].values.toArray(); - - if (!isNaN(timeIndex!)) { - xvals = xvals.map(v => v / 1000); - } - - plotData.push(xvals); - - for (let i = 0; i < data.fields.length; i++) { - const field = data.fields[i]; - - // already handled time and we ignore non-numeric fields - if (i === timeIndex || field.type !== FieldType.number) { - continue; - } - - let values = field.values.toArray(); - - if (field.config.custom?.nullValues === 'asZero') { - values = values.map(v => (v === null ? 0 : v)); - } - - plotData.push(values); - } - - return plotData; -}; - -const isPlottingTime = (config: uPlot.Options) => { +const isPlottingTime = (config: Options) => { let isTimeSeries = false; if (!config.scales) { @@ -93,7 +60,7 @@ const isPlottingTime = (config: uPlot.Options) => { * Based on two config objects indicates whether or not uPlot needs reinitialisation * This COULD be done based on data frames, but keeping it this way for now as a simplification */ -export const shouldInitialisePlot = (prevConfig?: uPlot.Options, config?: uPlot.Options) => { +export const shouldInitialisePlot = (prevConfig?: Options, config?: Options) => { if (!config && !prevConfig) { return false; } diff --git a/yarn.lock b/yarn.lock index 6c439642334..d084e07ce59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26919,10 +26919,10 @@ update-notifier@^2.5.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" -uplot@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.3.0.tgz#c805e632c9dc0a2f47041fa0431996cbb42de83a" - integrity sha512-15EIwgOYdTeX6YXRJK6u3sq/gtFFa8ICdQROTeQBStmekhGgl8MixhL6pO66pmxPuzaJUrfIa+o5gvzttMF5rw== +uplot@1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.4.4.tgz#ab30da26b1e2d432df5bc43389e70399787f0448" + integrity sha512-vgV84+by3fGTU4bdpffSvA9FX8ide6MsmlBzOASPDdZCquXmCA+T2qodeNdnBen+7YOeqD9H91epVnF0dQgVKw== upper-case-first@^1.1.0, upper-case-first@^1.1.2: version "1.1.2"