grafana/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts

294 lines
8.0 KiB
TypeScript
Raw Normal View History

import { merge } from 'lodash';
import uPlot, { Cursor, Band, Hooks, Select, AlignedData, Padding, Series } from 'uplot';
import { DataFrame, DefaultTimeZone, Field, getTimeZoneInfo, GrafanaTheme2, TimeRange, TimeZone } from '@grafana/data';
import { AxisPlacement, VizOrientation } from '@grafana/schema';
import { FacetedData, PlotConfig } from '../types';
import { DEFAULT_PLOT_CONFIG, getStackingBands, pluginLog, StackingGroup } from '../utils';
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
const cursorDefaults: Cursor = {
// prevent client-side zoom from triggering at the end of a selection
drag: { setScale: false },
points: {
/*@ts-ignore*/
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2,
/*@ts-ignore*/
width: (u, seriesIdx, size) => size / 4,
},
focus: {
prox: 30,
},
};
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
2021-07-29 09:31:07 +08:00
export class UPlotConfigBuilder {
readonly uid = Math.random().toString(36).slice(2);
series: UPlotSeriesBuilder[] = [];
private axes: Record<string, UPlotAxisBuilder> = {};
readonly scales: UPlotScaleBuilder[] = [];
private bands: Band[] = [];
private stackingGroups: StackingGroup[] = [];
private cursor: Cursor | undefined;
2021-05-05 16:44:31 +08:00
private select: uPlot.Select | undefined;
private hasLeftAxis = false;
private hooks: Hooks.Arrays = {};
private tz: string | undefined = undefined;
private mode: uPlot.Mode = 1;
private frames: DataFrame[] | undefined = undefined;
// to prevent more than one threshold per scale
private thresholds: Record<string, UPlotThresholdOptions> = {};
private padding?: Padding = undefined;
private cachedConfig?: PlotConfig;
2021-07-29 09:31:07 +08:00
prepData: PrepData | undefined = undefined;
2021-05-05 16:44:31 +08:00
constructor(timeZone: TimeZone = DefaultTimeZone) {
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
}
GraphNG - shared cursor (#33433) * Initial work * WIP add cursor in debug panel * shared cursor.sync filter * explicit uplot events * explicit uplot events * uplot events * uplot events * depend on master uplot * sync sync sync * Fix merge * Get rid of PlotSyncContext and sync tooltip positions * make sync optional * Improve shared tooltip positioning * Plugins: add level and signature badges to plugin details page (#33553) * feat(grafana-ui): badge can accept react node for text, add shield-exclamation to icons * feat(plugins): add PluginSignatureType type * feat(pluginpage): introduce PluginSignatureDetailsBadge. Fix sidebar icon margin * feat(pluginlistpage): update filterinput placeholder, introduce filter by plugin type * Variables: Removes the never refresh option (#33533) * Variables: Removes the never refresh option * Tests: fixes DashboardModel repeat tests * Tests: fixs snapshots * Tests: fixes processVariable test * Tests: fixes DashboardModel tests * PageLayout: Fixes max-width breakpoint so that it triggers only when there is room for margin+ (#33558) * Alerting: Remove datasource (name) from migration (#33544) no longer needed as of https://github.com/grafana/grafana/pull/33416 for https://github.com/grafana/alerting-squad/issues/126 * Library panels: Adds description to library panels tab (#33428) * CodeOwners: Set owners of unified alerting migration (#33571) * ButtonSelect: updates component with the new theme model (#33565) * EmptySearchResult: updates component with the new theme model (#33573) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design (#33575) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design * Fixed font weight * Removed comment * Update * gitignore: Ignore files for accesscontrol provisioning (#33577) * Alerting/metrics (#33547) * moves alerting metrics to their own pkg * adds grafana_alerting_alerts (by state) metric * alerts_received_{total,invalid} * embed alertmanager alerting struct in ng metrics & remove duplicated notification metrics (already embed alertmanager notifier metrics) * use silence metrics from alertmanager lib * fix - manager has metrics * updates ngalert tests * comment lint Signed-off-by: Owen Diehl <ow.diehl@gmail.com> * cleaner prom registry code * removes ngalert global metrics * new registry use in all tests * ngalert metrics impl service, hack testinfra code to prevent duplicate metric registrations * nilmetrics unexported * Add note to Snapshot API doc to specify that user has to provide the entire dashboard model (#33572) * Added note as suggested by Macus E. * Update docs/sources/http_api/snapshot.md Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Alerting: backend "ng" code cleanup (#33578) * AlertMigration: remove alert_rule UID db check (#33568) do not believe this is needed due to uniqueness promised by shortid lib since there is no provisioning yet. https://github.com/teris-io/shortid * Live: persisting last message in cache for broadcast scope (#32938) * Alerting: Load annotations from rule into State cache (#33542) for https://github.com/grafana/alerting-squad/issues/127 * add template for dashboard url parameters (#33549) * Update dashboard-links.md parameters with plain text like `var-something=value` can make confusion. template it to clarify . * describe way for template link. * AlertingMigration: Create alert_rule_version entry (#33585) Create the alert rule version entry during the migration so it is consistent with rules created via api. for https://github.com/grafana/alerting-squad/issues/123 * Build: Fix with cleanup call maybe? (#33590) * add selector for code editor (#33554) * broadcast over eventBus * broadcasting to eventbus (but not useing it yet) * merge master * moved to context * fix yarn.lock * update snapshot * Fix direct state mutation * Persist location state on partial updates * GraphNG- use getStream rather than subscribe * Sync LegacyGraphHoverEvent with GraphNG * Chenge plotRef signature * use subscription * subscription * one fewer file * Update types * Remove unnecessary filtering * Disable cursor sync when in edit mode * GraphNG - bring back logging Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com> Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Kyle Brandt <kyle@grafana.com> Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Uchechukwu Obasi <obasiuche62@gmail.com> Co-authored-by: gamab <gamab@users.noreply.github.com> Co-authored-by: Owen Diehl <ow.diehl@gmail.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Alexander Emelin <frvzmb@gmail.com> Co-authored-by: Nagle Zhang <nagle.zhang@sap.com> Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
2021-05-10 20:24:23 +08:00
// Exposed to let the container know the primary scale keys
scaleKeys: [string, string] = ['', ''];
addHook<T extends keyof Hooks.Defs>(type: T, hook: Hooks.Defs[T]) {
pluginLog('UPlotConfigBuilder', false, 'addHook', type);
if (!this.hooks[type]) {
this.hooks[type] = [];
}
this.hooks[type].push(hook);
}
addThresholds(options: UPlotThresholdOptions) {
if (!this.thresholds[options.scaleKey]) {
this.thresholds[options.scaleKey] = options;
this.addHook('drawClear', getThresholdsDrawHook(options));
}
}
addAxis(props: AxisProps) {
props.placement = props.placement ?? AxisPlacement.Auto;
props.grid = props.grid ?? {};
let scaleKey = props.scaleKey;
if (scaleKey === 'x') {
scaleKey += props.timeZone ?? '';
}
if (this.axes[scaleKey]) {
this.axes[scaleKey].merge(props);
return;
}
// Handle auto placement logic
if (props.placement === AxisPlacement.Auto) {
props.placement = this.hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left;
}
if (props.placement === AxisPlacement.Left) {
this.hasLeftAxis = true;
}
if (props.placement === AxisPlacement.Hidden) {
props.grid.show = false;
props.size = 0;
}
this.axes[scaleKey] = new UPlotAxisBuilder(props);
}
getAxisPlacement(scaleKey: string): AxisPlacement {
const axis = this.axes[scaleKey];
return axis?.props.placement! ?? AxisPlacement.Left;
}
setCursor(cursor?: Cursor) {
this.cursor = merge({}, this.cursor, cursor);
}
setMode(mode: uPlot.Mode) {
this.mode = mode;
}
2021-05-05 16:44:31 +08:00
setSelect(select: Select) {
this.select = select;
}
addSeries(props: SeriesProps) {
this.series.push(new UPlotSeriesBuilder(props));
}
getSeries() {
return this.series;
}
/** Add or update the scale with the scale key */
addScale(props: ScaleProps) {
const current = this.scales.find((v) => v.props.scaleKey === props.scaleKey);
if (current) {
current.merge(props);
return;
}
this.scales.push(new UPlotScaleBuilder(props));
}
addBand(band: Band) {
this.bands.push(band);
}
setStackingGroups(groups: StackingGroup[]) {
this.stackingGroups = groups;
}
getStackingGroups() {
return this.stackingGroups;
}
setPrepData(prepData: PreDataStacked) {
this.prepData = (frames) => {
this.frames = frames;
return prepData(frames, this.getStackingGroups());
};
2021-07-29 09:31:07 +08:00
}
setPadding(padding: Padding) {
this.padding = padding;
}
getConfig() {
if (this.cachedConfig) {
return this.cachedConfig;
}
2021-08-05 05:13:11 +08:00
const config: PlotConfig = {
...DEFAULT_PLOT_CONFIG,
mode: this.mode,
2021-08-05 05:13:11 +08:00
series: [
this.mode === 2
? (null as unknown as Series)
: {
value: () => '',
},
2021-08-05 05:13:11 +08:00
],
};
config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map((a) => a.getConfig());
config.series = [...config.series, ...this.series.map((s) => s.getConfig())];
config.scales = this.scales.reduce((acc, s) => {
return { ...acc, ...s.getConfig() };
}, {});
2020-12-12 03:01:55 +08:00
config.hooks = this.hooks;
config.select = this.select;
const pointColorFn =
(alphaHex = '') =>
(u: uPlot, seriesIdx: number) => {
/*@ts-ignore*/
let s = u.series[seriesIdx].points._stroke;
// interpolate for gradients/thresholds
if (typeof s !== 'string') {
let field = this.frames![0].fields[seriesIdx];
s = field.display!(field.values[u.cursor.idxs![seriesIdx]!]).color!;
}
return s + alphaHex;
};
config.cursor = merge(
{},
cursorDefaults,
{
points: {
stroke: pointColorFn('80'),
fill: pointColorFn(),
},
},
this.cursor
);
2020-12-12 03:01:55 +08:00
config.tzDate = this.tzDate;
if (Array.isArray(this.padding)) {
config.padding = this.padding;
}
this.stackingGroups.forEach((group) => {
getStackingBands(group).forEach((band) => {
this.addBand(band);
});
});
if (this.bands.length) {
config.bands = this.bands;
}
this.cachedConfig = config;
return config;
}
private tzDate = (ts: number) => {
let date = new Date(ts);
return this.tz ? uPlot.tzDate(date, this.tz) : date;
};
private ensureNonOverlappingAxes(axes: UPlotAxisBuilder[]): UPlotAxisBuilder[] {
const xAxis = axes.find((a) => a.props.scaleKey === 'x');
const axesWithoutGridSet = axes.filter((a) => a.props.grid?.show === undefined);
const firstValueAxisIdx = axesWithoutGridSet.findIndex(
(a) =>
a.props.placement === AxisPlacement.Left ||
a.props.placement === AxisPlacement.Right ||
(a.props.placement === AxisPlacement.Bottom && a !== xAxis)
);
// For all axes with no grid set, set the grid automatically (grid only for first left axis )
for (let i = 0; i < axesWithoutGridSet.length; i++) {
if (axesWithoutGridSet[i] === xAxis || i === firstValueAxisIdx) {
axesWithoutGridSet[i].props.grid!.show = true;
} else {
axesWithoutGridSet[i].props.grid!.show = false;
}
}
return axes;
}
}
export type Renderers = Array<{
fieldMap: Record<string, string>;
indicesOnly: string[];
init: (config: UPlotConfigBuilder, fieldIndices: Record<string, number>) => void;
}>;
/** @alpha */
type UPlotConfigPrepOpts<T extends Record<string, unknown> = {}> = {
frame: DataFrame;
theme: GrafanaTheme2;
timeZones: TimeZone[];
getTimeRange: () => TimeRange;
allFrames: DataFrame[];
renderers?: Renderers;
tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps;
tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps;
hoverProximity?: number;
orientation?: VizOrientation;
2025-09-24 05:52:36 +08:00
annotations?: DataFrame[];
} & T;
/** @alpha */
export type UPlotConfigPrepFn<T extends {} = {}> = (opts: UPlotConfigPrepOpts<T>) => UPlotConfigBuilder;