graph.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. import 'vendor/flot/jquery.flot';
  2. import 'vendor/flot/jquery.flot.selection';
  3. import 'vendor/flot/jquery.flot.time';
  4. import 'vendor/flot/jquery.flot.stack';
  5. import 'vendor/flot/jquery.flot.stackpercent';
  6. import 'vendor/flot/jquery.flot.fillbelow';
  7. import 'vendor/flot/jquery.flot.crosshair';
  8. import 'vendor/flot/jquery.flot.dashes';
  9. import './jquery.flot.events';
  10. import $ from 'jquery';
  11. import { clone, find, flatten, isUndefined, map, max as _max, min as _min, sortBy as _sortBy, toNumber } from 'lodash';
  12. import React from 'react';
  13. import ReactDOM from 'react-dom';
  14. import {
  15. DataFrame,
  16. DataFrameView,
  17. DataHoverClearEvent,
  18. DataHoverEvent,
  19. DataHoverPayload,
  20. FieldDisplay,
  21. FieldType,
  22. formattedValueToString,
  23. getDisplayProcessor,
  24. getFlotPairsConstant,
  25. getTimeField,
  26. getValueFormat,
  27. hasLinks,
  28. LegacyEventHandler,
  29. LegacyGraphHoverClearEvent,
  30. LegacyGraphHoverEvent,
  31. LegacyGraphHoverEventPayload,
  32. LinkModelSupplier,
  33. PanelEvents,
  34. toUtc,
  35. } from '@grafana/data';
  36. import { graphTickFormatter, graphTimeFormat, IconName, MenuItemProps, MenuItemsGroup } from '@grafana/ui';
  37. import { coreModule } from 'app/angular/core_module';
  38. import config from 'app/core/config';
  39. import { updateLegendValues } from 'app/core/core';
  40. import { ContextSrv } from 'app/core/services/context_srv';
  41. import { provideTheme } from 'app/core/utils/ConfigProvider';
  42. import { tickStep } from 'app/core/utils/ticks';
  43. import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  44. import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
  45. import { DashboardModel } from '../../../features/dashboard/state';
  46. import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
  47. import { GraphLegendProps, Legend } from './Legend/Legend';
  48. import { alignYLevel } from './align_yaxes';
  49. import { EventManager } from './event_manager';
  50. import GraphTooltip from './graph_tooltip';
  51. import { convertToHistogramData } from './histogram';
  52. import { GraphCtrl } from './module';
  53. import { ThresholdManager } from './threshold_manager';
  54. import { TimeRegionManager } from './time_region_manager';
  55. import { isLegacyGraphHoverEvent } from './utils';
  56. const LegendWithThemeProvider = provideTheme(Legend);
  57. class GraphElement {
  58. ctrl: GraphCtrl;
  59. contextMenu: GraphContextMenuCtrl;
  60. tooltip: any;
  61. dashboard: DashboardModel;
  62. annotations: object[];
  63. panel: any;
  64. plot: any;
  65. sortedSeries?: any[];
  66. data: any[] = [];
  67. panelWidth: number;
  68. eventManager: EventManager;
  69. thresholdManager: ThresholdManager;
  70. timeRegionManager: TimeRegionManager;
  71. declare legendElem: HTMLElement;
  72. constructor(
  73. private scope: any,
  74. private elem: JQuery & {
  75. bind(eventType: string, handler: (eventObject: JQueryEventObject, ...args: any[]) => any): JQuery; // need to extend with Plot
  76. },
  77. private timeSrv: TimeSrv
  78. ) {
  79. this.ctrl = scope.ctrl;
  80. this.contextMenu = scope.ctrl.contextMenuCtrl;
  81. this.dashboard = this.ctrl.dashboard;
  82. this.panel = this.ctrl.panel;
  83. this.annotations = [];
  84. this.panelWidth = 0;
  85. this.eventManager = new EventManager(this.ctrl);
  86. this.thresholdManager = new ThresholdManager(this.ctrl);
  87. this.timeRegionManager = new TimeRegionManager(this.ctrl);
  88. // @ts-ignore
  89. this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
  90. return this.sortedSeries;
  91. });
  92. // panel events
  93. this.ctrl.events.on(PanelEvents.panelTeardown, this.onPanelTeardown.bind(this));
  94. this.ctrl.events.on(PanelEvents.render, this.onRender.bind(this));
  95. // global events
  96. // Using old way here to use the scope unsubscribe model as the new $on function does not take scope
  97. this.ctrl.dashboard.events.on(LegacyGraphHoverEvent.type, this.onGraphHover.bind(this), this.scope);
  98. this.ctrl.dashboard.events.on(LegacyGraphHoverClearEvent.type, this.onGraphHoverClear.bind(this), this.scope);
  99. this.ctrl.dashboard.events.on(DataHoverEvent.type, this.onGraphHover.bind(this), this.scope);
  100. this.ctrl.dashboard.events.on(DataHoverClearEvent.type, this.onGraphHoverClear.bind(this), this.scope);
  101. // plot events
  102. this.elem.bind('plotselected', this.onPlotSelected.bind(this));
  103. this.elem.bind('plotclick', this.onPlotClick.bind(this));
  104. // get graph legend element
  105. if (this.elem && this.elem.parent) {
  106. this.legendElem = this.elem.parent().find('.graph-legend')[0];
  107. }
  108. }
  109. onRender(renderData: any[]) {
  110. this.data = renderData || this.data;
  111. if (!this.data) {
  112. return;
  113. }
  114. this.annotations = this.ctrl.annotations || [];
  115. this.buildFlotPairs(this.data);
  116. const graphHeight = this.ctrl.height;
  117. updateLegendValues(this.data, this.panel, graphHeight);
  118. if (!this.panel.legend.show) {
  119. if (this.legendElem.hasChildNodes()) {
  120. ReactDOM.unmountComponentAtNode(this.legendElem);
  121. }
  122. this.renderPanel();
  123. return;
  124. }
  125. const { values, min, max, avg, current, total } = this.panel.legend;
  126. const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend;
  127. const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero };
  128. const valueOptions = { values, min, max, avg, current, total };
  129. const legendProps: GraphLegendProps = {
  130. seriesList: this.data,
  131. hiddenSeries: this.ctrl.hiddenSeries,
  132. ...legendOptions,
  133. ...valueOptions,
  134. onToggleSeries: this.ctrl.onToggleSeries,
  135. onToggleSort: this.ctrl.onToggleSort,
  136. onColorChange: this.ctrl.onColorChange,
  137. onToggleAxis: this.ctrl.onToggleAxis,
  138. };
  139. const legendReactElem = React.createElement(LegendWithThemeProvider, legendProps);
  140. ReactDOM.render(legendReactElem, this.legendElem, () => this.renderPanel());
  141. }
  142. onGraphHover(evt: LegacyGraphHoverEventPayload | DataHoverPayload) {
  143. // ignore other graph hover events if shared tooltip is disabled
  144. if (!this.dashboard.sharedTooltipModeEnabled()) {
  145. return;
  146. }
  147. if (isLegacyGraphHoverEvent(evt)) {
  148. // ignore if we are the emitter
  149. if (!this.plot || evt.panel?.id === this.panel.id || this.ctrl.otherPanelInFullscreenMode()) {
  150. return;
  151. }
  152. this.tooltip.show(evt.pos);
  153. }
  154. // DataHoverEvent can come from multiple panels that doesn't include x position
  155. if (!evt.point?.time) {
  156. return;
  157. }
  158. this.tooltip.show({ x: evt.point.time, panelRelY: evt.point.panelRelY ?? 1 });
  159. }
  160. onPanelTeardown() {
  161. if (this.plot) {
  162. this.plot.destroy();
  163. this.plot = null;
  164. }
  165. this.tooltip.destroy();
  166. this.elem.off();
  167. this.elem.remove();
  168. ReactDOM.unmountComponentAtNode(this.legendElem);
  169. }
  170. onGraphHoverClear(handler: LegacyEventHandler<any>) {
  171. if (this.plot) {
  172. this.tooltip.clear(this.plot);
  173. }
  174. }
  175. onPlotSelected(event: JQueryEventObject, ranges: any) {
  176. if (this.panel.xaxis.mode !== 'time') {
  177. // Skip if panel in histogram or series mode
  178. this.plot.clearSelection();
  179. return;
  180. }
  181. if ((ranges.ctrlKey || ranges.metaKey) && this.dashboard.canAddAnnotations()) {
  182. // Add annotation
  183. setTimeout(() => {
  184. this.eventManager.updateTime(ranges.xaxis);
  185. }, 100);
  186. } else {
  187. this.scope.$apply(() => {
  188. this.timeSrv.setTime({
  189. from: toUtc(ranges.xaxis.from),
  190. to: toUtc(ranges.xaxis.to),
  191. });
  192. });
  193. }
  194. }
  195. getContextMenuItemsSupplier = (
  196. flotPosition: { x: number; y: number },
  197. linksSupplier?: LinkModelSupplier<FieldDisplay>
  198. ): (() => MenuItemsGroup[]) => {
  199. return () => {
  200. // Fixed context menu items
  201. const items: MenuItemsGroup[] = this.dashboard.canAddAnnotations()
  202. ? [
  203. {
  204. items: [
  205. {
  206. label: 'Add annotation',
  207. ariaLabel: 'Add annotation',
  208. icon: 'comment-alt',
  209. onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }),
  210. },
  211. ],
  212. },
  213. ]
  214. : [];
  215. if (!linksSupplier) {
  216. return items;
  217. }
  218. const dataLinks = [
  219. {
  220. items: linksSupplier.getLinks(this.panel.replaceVariables).map<MenuItemProps>((link) => {
  221. return {
  222. label: link.title,
  223. ariaLabel: link.title,
  224. url: link.href,
  225. target: link.target,
  226. icon: `${link.target === '_self' ? 'link' : 'external-link-alt'}` as IconName,
  227. onClick: link.onClick,
  228. };
  229. }),
  230. },
  231. ];
  232. return [...items, ...dataLinks];
  233. };
  234. };
  235. onPlotClick(event: JQueryEventObject, pos: any, item: any) {
  236. const scrollContextElement = this.elem.closest('.view') ? this.elem.closest('.view').get()[0] : null;
  237. const contextMenuSourceItem = item;
  238. if (this.panel.xaxis.mode !== 'time') {
  239. // Skip if panel in histogram or series mode
  240. return;
  241. }
  242. if (pos.ctrlKey || pos.metaKey) {
  243. // Skip if range selected (added in "plotselected" event handler)
  244. if (pos.x !== pos.x1) {
  245. return;
  246. }
  247. // skip if dashboard is not saved yet (exists in db) or user cannot edit
  248. if (!this.dashboard.id || !this.dashboard.canAddAnnotations()) {
  249. return;
  250. }
  251. setTimeout(() => {
  252. this.eventManager.updateTime({ from: pos.x, to: null });
  253. }, 100);
  254. return;
  255. } else {
  256. this.tooltip.clear(this.plot);
  257. let linksSupplier: LinkModelSupplier<FieldDisplay> | undefined;
  258. if (item) {
  259. // pickup y-axis index to know which field's config to apply
  260. const yAxisConfig = this.panel.yaxes[item.series.yaxis.n === 2 ? 1 : 0];
  261. const dataFrame = this.ctrl.dataList[item.series.dataFrameIndex];
  262. const field = dataFrame.fields[item.series.fieldIndex];
  263. const dataIndex = this.getDataIndexWithNullValuesCorrection(item, dataFrame);
  264. let links: any[] = this.panel.options.dataLinks || [];
  265. const hasLinksValue = hasLinks(field);
  266. if (hasLinksValue) {
  267. // Append the configured links to the panel datalinks
  268. links = [...links, ...field.config.links!];
  269. }
  270. const fieldConfig = {
  271. decimals: yAxisConfig.decimals,
  272. links,
  273. };
  274. const fieldDisplay = getDisplayProcessor({
  275. field: { config: fieldConfig, type: FieldType.number },
  276. theme: config.theme2,
  277. timeZone: this.dashboard.getTimezone(),
  278. })(field.values.get(dataIndex));
  279. linksSupplier = links.length
  280. ? getFieldLinksSupplier({
  281. display: fieldDisplay,
  282. name: field.name,
  283. view: new DataFrameView(dataFrame),
  284. rowIndex: dataIndex,
  285. colIndex: item.series.fieldIndex,
  286. field: fieldConfig,
  287. hasLinks: hasLinksValue,
  288. })
  289. : undefined;
  290. }
  291. this.scope.$apply(() => {
  292. // Setting nearest CustomScrollbar element as a scroll context for graph context menu
  293. this.contextMenu.setScrollContextElement(scrollContextElement);
  294. this.contextMenu.setSource(contextMenuSourceItem);
  295. this.contextMenu.setMenuItemsSupplier(this.getContextMenuItemsSupplier(pos, linksSupplier) as any);
  296. this.contextMenu.toggleMenu(pos);
  297. });
  298. }
  299. }
  300. getDataIndexWithNullValuesCorrection(item: any, dataFrame: DataFrame): number {
  301. /** This is one added to handle the scenario where we have null values in
  302. * the time series data and the: "visualization options -> null value"
  303. * set to "connected". In this scenario we will get the wrong dataIndex.
  304. *
  305. * https://github.com/grafana/grafana/issues/22651
  306. */
  307. const { datapoint, dataIndex } = item;
  308. if (!Array.isArray(datapoint) || datapoint.length === 0) {
  309. return dataIndex;
  310. }
  311. const ts = datapoint[0];
  312. const { timeField } = getTimeField(dataFrame);
  313. if (!timeField || !timeField.values) {
  314. return dataIndex;
  315. }
  316. const field = timeField.values.get(dataIndex);
  317. if (field === ts) {
  318. return dataIndex;
  319. }
  320. const correctIndex = timeField.values.toArray().findIndex((value) => value === ts);
  321. return correctIndex > -1 ? correctIndex : dataIndex;
  322. }
  323. shouldAbortRender() {
  324. if (!this.data) {
  325. return true;
  326. }
  327. if (this.panelWidth === 0) {
  328. return true;
  329. }
  330. return false;
  331. }
  332. drawHook(plot: any) {
  333. // add left axis labels
  334. if (this.panel.yaxes[0].label && this.panel.yaxes[0].show) {
  335. $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
  336. .text(this.panel.yaxes[0].label)
  337. .appendTo(this.elem);
  338. }
  339. // add right axis labels
  340. if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) {
  341. $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
  342. .text(this.panel.yaxes[1].label)
  343. .appendTo(this.elem);
  344. }
  345. const { dataWarning } = this.ctrl;
  346. if (dataWarning) {
  347. const msg = $(`<div class="datapoints-warning flot-temp-elem">${dataWarning.title}</div>`);
  348. if (dataWarning.action) {
  349. $(`<button class="btn btn-secondary">${dataWarning.actionText}</button>`)
  350. .click(dataWarning.action)
  351. .appendTo(msg);
  352. }
  353. msg.appendTo(this.elem);
  354. }
  355. this.thresholdManager.draw(plot);
  356. this.timeRegionManager.draw(plot);
  357. }
  358. processOffsetHook(plot: any, gridMargin: { left: number; right: number }) {
  359. const left = this.panel.yaxes[0];
  360. const right = this.panel.yaxes[1];
  361. if (left.show && left.label) {
  362. gridMargin.left = 20;
  363. }
  364. if (right.show && right.label) {
  365. gridMargin.right = 20;
  366. }
  367. // apply y-axis min/max options
  368. const yaxis = plot.getYAxes();
  369. for (let i = 0; i < yaxis.length; i++) {
  370. const axis: any = yaxis[i];
  371. const panelOptions = this.panel.yaxes[i];
  372. axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;
  373. axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;
  374. }
  375. }
  376. processRangeHook(plot: any) {
  377. const yAxes = plot.getYAxes();
  378. const align = this.panel.yaxis.align || false;
  379. if (yAxes.length > 1 && align === true) {
  380. const level = this.panel.yaxis.alignLevel || 0;
  381. alignYLevel(yAxes, parseFloat(level));
  382. }
  383. }
  384. // Series could have different timeSteps,
  385. // let's find the smallest one so that bars are correctly rendered.
  386. // In addition, only take series which are rendered as bars for this.
  387. getMinTimeStepOfSeries(data: any[]) {
  388. let min = Number.MAX_VALUE;
  389. for (let i = 0; i < data.length; i++) {
  390. if (!data[i].stats.timeStep) {
  391. continue;
  392. }
  393. if (this.panel.bars) {
  394. if (data[i].bars && data[i].bars.show === false) {
  395. continue;
  396. }
  397. } else {
  398. if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {
  399. continue;
  400. }
  401. }
  402. if (data[i].stats.timeStep < min) {
  403. min = data[i].stats.timeStep;
  404. }
  405. }
  406. return min;
  407. }
  408. // Function for rendering panel
  409. renderPanel() {
  410. this.panelWidth = this.elem.width() ?? 0;
  411. if (this.shouldAbortRender()) {
  412. return;
  413. }
  414. // give space to alert editing
  415. this.thresholdManager.prepare(this.elem, this.data);
  416. // un-check dashes if lines are unchecked
  417. this.panel.dashes = this.panel.lines ? this.panel.dashes : false;
  418. // Populate element
  419. const options: any = this.buildFlotOptions(this.panel);
  420. this.prepareXAxis(options, this.panel);
  421. this.configureYAxisOptions(this.data, options);
  422. this.thresholdManager.addFlotOptions(options, this.panel);
  423. this.timeRegionManager.addFlotOptions(options, this.panel);
  424. this.eventManager.addFlotEvents(this.annotations, options);
  425. this.sortedSeries = this.sortSeries(this.data, this.panel);
  426. this.callPlot(options, true);
  427. }
  428. buildFlotPairs(data: any) {
  429. for (let i = 0; i < data.length; i++) {
  430. const series = data[i];
  431. series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode);
  432. if (series.transform === 'constant') {
  433. series.data = getFlotPairsConstant(series.data, this.ctrl.range!);
  434. }
  435. // if hidden remove points and disable stack
  436. if (this.ctrl.hiddenSeries[series.alias]) {
  437. series.data = [];
  438. series.stack = false;
  439. }
  440. }
  441. }
  442. prepareXAxis(options: any, panel: any) {
  443. switch (panel.xaxis.mode) {
  444. case 'series': {
  445. options.series.bars.barWidth = 0.7;
  446. options.series.bars.align = 'center';
  447. for (let i = 0; i < this.data.length; i++) {
  448. const series = this.data[i];
  449. series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
  450. }
  451. this.addXSeriesAxis(options);
  452. break;
  453. }
  454. case 'histogram': {
  455. let bucketSize: number;
  456. if (this.data.length) {
  457. let histMin = _min(map(this.data, (s) => s.stats.min));
  458. let histMax = _max(map(this.data, (s) => s.stats.max));
  459. const ticks = panel.xaxis.buckets || this.panelWidth / 50;
  460. if (panel.xaxis.min != null) {
  461. const isInvalidXaxisMin = tickStep(panel.xaxis.min, histMax, ticks) <= 0;
  462. histMin = isInvalidXaxisMin ? histMin : panel.xaxis.min;
  463. }
  464. if (panel.xaxis.max != null) {
  465. const isInvalidXaxisMax = tickStep(histMin, panel.xaxis.max, ticks) <= 0;
  466. histMax = isInvalidXaxisMax ? histMax : panel.xaxis.max;
  467. }
  468. bucketSize = tickStep(histMin, histMax, ticks);
  469. options.series.bars.barWidth = bucketSize * 0.8;
  470. this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);
  471. } else {
  472. bucketSize = 0;
  473. }
  474. this.addXHistogramAxis(options, bucketSize);
  475. break;
  476. }
  477. case 'table': {
  478. options.series.bars.barWidth = 0.7;
  479. options.series.bars.align = 'center';
  480. this.addXTableAxis(options);
  481. break;
  482. }
  483. default: {
  484. options.series.bars.barWidth = this.getMinTimeStepOfSeries(this.data) / 1.5;
  485. this.addTimeAxis(options);
  486. break;
  487. }
  488. }
  489. }
  490. callPlot(options: any, incrementRenderCounter: boolean) {
  491. try {
  492. this.plot = $.plot(this.elem, this.sortedSeries, options);
  493. if (this.ctrl.renderError) {
  494. delete this.ctrl.error;
  495. }
  496. } catch (e) {
  497. console.error('flotcharts error', e);
  498. this.ctrl.error = e.message || 'Render Error';
  499. this.ctrl.renderError = true;
  500. }
  501. if (incrementRenderCounter) {
  502. this.ctrl.renderingCompleted();
  503. }
  504. }
  505. buildFlotOptions(panel: any) {
  506. let gridColor = '#c8c8c8';
  507. if (config.bootData.user.lightTheme === true) {
  508. gridColor = '#a1a1a1';
  509. }
  510. const stack = panel.stack ? true : null;
  511. const options: any = {
  512. hooks: {
  513. draw: [this.drawHook.bind(this)],
  514. processOffset: [this.processOffsetHook.bind(this)],
  515. processRange: [this.processRangeHook.bind(this)],
  516. },
  517. legend: { show: false },
  518. series: {
  519. stackpercent: panel.stack ? panel.percentage : false,
  520. stack: panel.percentage ? null : stack,
  521. lines: {
  522. show: panel.lines,
  523. zero: false,
  524. fill: this.translateFillOption(panel.fill),
  525. fillColor: this.getFillGradient(panel.fillGradient),
  526. lineWidth: panel.dashes ? 0 : panel.linewidth,
  527. steps: panel.steppedLine,
  528. },
  529. dashes: {
  530. show: panel.dashes,
  531. lineWidth: panel.linewidth,
  532. dashLength: [panel.dashLength, panel.spaceLength],
  533. },
  534. bars: {
  535. show: panel.bars,
  536. fill: 1,
  537. barWidth: 1,
  538. zero: false,
  539. lineWidth: 0,
  540. },
  541. points: {
  542. show: panel.points,
  543. fill: 1,
  544. fillColor: false,
  545. radius: panel.points ? panel.pointradius : 2,
  546. },
  547. shadowSize: 0,
  548. },
  549. yaxes: [],
  550. xaxis: {},
  551. grid: {
  552. minBorderMargin: 0,
  553. markings: [],
  554. backgroundColor: null,
  555. borderWidth: 0,
  556. hoverable: true,
  557. clickable: true,
  558. color: gridColor,
  559. margin: { left: 0, right: 0 },
  560. labelMarginX: 0,
  561. mouseActiveRadius: 30,
  562. },
  563. selection: {
  564. mode: 'x',
  565. color: '#666',
  566. },
  567. crosshair: {
  568. mode: 'x',
  569. },
  570. };
  571. return options;
  572. }
  573. sortSeries(series: any, panel: any) {
  574. const sortBy = panel.legend.sort;
  575. const sortOrder = panel.legend.sortDesc;
  576. const haveSortBy = sortBy !== null && sortBy !== undefined && panel.legend[sortBy];
  577. const haveSortOrder = sortOrder !== null && sortOrder !== undefined;
  578. const shouldSortBy = panel.stack && haveSortBy && haveSortOrder && panel.legend.alignAsTable;
  579. const sortDesc = panel.legend.sortDesc === true ? -1 : 1;
  580. if (shouldSortBy) {
  581. return _sortBy(series, (s) => s.stats[sortBy] * sortDesc);
  582. } else {
  583. return _sortBy(series, (s) => s.zindex);
  584. }
  585. }
  586. getFillGradient(amount: number) {
  587. if (!amount) {
  588. return null;
  589. }
  590. return {
  591. colors: [{ opacity: 0.0 }, { opacity: amount / 10 }],
  592. };
  593. }
  594. translateFillOption(fill: number) {
  595. if (this.panel.percentage && this.panel.stack) {
  596. return fill === 0 ? 0.001 : fill / 10;
  597. } else {
  598. return fill / 10;
  599. }
  600. }
  601. addTimeAxis(options: any) {
  602. const ticks = this.panelWidth / 100;
  603. const min = isUndefined(this.ctrl.range!.from) ? null : this.ctrl.range!.from.valueOf();
  604. const max = isUndefined(this.ctrl.range!.to) ? null : this.ctrl.range!.to.valueOf();
  605. options.xaxis = {
  606. timezone: this.dashboard.getTimezone(),
  607. show: this.panel.xaxis.show,
  608. mode: 'time',
  609. min: min,
  610. max: max,
  611. label: 'Datetime',
  612. ticks: ticks,
  613. timeformat: graphTimeFormat(ticks, min, max),
  614. tickFormatter: graphTickFormatter,
  615. };
  616. }
  617. addXSeriesAxis(options: any) {
  618. const ticks = map(this.data, (series, index) => {
  619. return [index + 1, series.alias];
  620. });
  621. options.xaxis = {
  622. timezone: this.dashboard.getTimezone(),
  623. show: this.panel.xaxis.show,
  624. mode: null,
  625. min: 0,
  626. max: ticks.length + 1,
  627. label: 'Datetime',
  628. ticks: ticks,
  629. };
  630. }
  631. addXHistogramAxis(options: any, bucketSize: number) {
  632. let ticks: number | number[];
  633. let min: number | undefined;
  634. let max: number | undefined;
  635. const defaultTicks = this.panelWidth / 50;
  636. if (this.data.length && bucketSize) {
  637. const tickValues = [];
  638. for (const d of this.data) {
  639. for (const point of d.data) {
  640. tickValues[point[0]] = true;
  641. }
  642. }
  643. ticks = Object.keys(tickValues).map((v) => Number(v));
  644. min = _min(ticks)!;
  645. max = _max(ticks)!;
  646. // Adjust tick step
  647. let tickStep = bucketSize;
  648. let ticksNum = Math.floor((max - min) / tickStep);
  649. while (ticksNum > defaultTicks) {
  650. tickStep = tickStep * 2;
  651. ticksNum = Math.ceil((max - min) / tickStep);
  652. }
  653. // Expand ticks for pretty view
  654. min = Math.floor(min / tickStep) * tickStep;
  655. // 1.01 is 101% - ensure we have enough space for last bar
  656. max = Math.ceil((max * 1.01) / tickStep) * tickStep;
  657. ticks = [];
  658. for (let i = min; i <= max; i += tickStep) {
  659. ticks.push(i);
  660. }
  661. } else {
  662. // Set defaults if no data
  663. ticks = defaultTicks / 2;
  664. min = 0;
  665. max = 1;
  666. }
  667. options.xaxis = {
  668. timezone: this.dashboard.getTimezone(),
  669. show: this.panel.xaxis.show,
  670. mode: null,
  671. min: min,
  672. max: max,
  673. label: 'Histogram',
  674. ticks: ticks,
  675. };
  676. // Use 'short' format for histogram values
  677. this.configureAxisMode(options.xaxis, 'short', null);
  678. }
  679. addXTableAxis(options: any) {
  680. let ticks = map(this.data, (series, seriesIndex) => {
  681. return map(series.datapoints, (point, pointIndex) => {
  682. const tickIndex = seriesIndex * series.datapoints.length + pointIndex;
  683. return [tickIndex + 1, point[1]];
  684. });
  685. });
  686. // @ts-ignore, potential bug? is this flattenDeep?
  687. ticks = flatten(ticks, true);
  688. options.xaxis = {
  689. timezone: this.dashboard.getTimezone(),
  690. show: this.panel.xaxis.show,
  691. mode: null,
  692. min: 0,
  693. max: ticks.length + 1,
  694. label: 'Datetime',
  695. ticks: ticks,
  696. };
  697. }
  698. configureYAxisOptions(data: any, options: any) {
  699. const defaults = {
  700. position: 'left',
  701. show: this.panel.yaxes[0].show,
  702. index: 1,
  703. logBase: this.panel.yaxes[0].logBase || 1,
  704. min: this.parseNumber(this.panel.yaxes[0].min),
  705. max: this.parseNumber(this.panel.yaxes[0].max),
  706. tickDecimals: this.panel.yaxes[0].decimals,
  707. };
  708. options.yaxes.push(defaults);
  709. if (find(data, { yaxis: 2 })) {
  710. const secondY = clone(defaults);
  711. secondY.index = 2;
  712. secondY.show = this.panel.yaxes[1].show;
  713. secondY.logBase = this.panel.yaxes[1].logBase || 1;
  714. secondY.position = 'right';
  715. secondY.min = this.parseNumber(this.panel.yaxes[1].min);
  716. secondY.max = this.parseNumber(this.panel.yaxes[1].max);
  717. secondY.tickDecimals = this.panel.yaxes[1].decimals;
  718. options.yaxes.push(secondY);
  719. this.applyLogScale(options.yaxes[1], data);
  720. this.configureAxisMode(
  721. options.yaxes[1],
  722. this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format,
  723. this.panel.yaxes[1].decimals
  724. );
  725. }
  726. this.applyLogScale(options.yaxes[0], data);
  727. this.configureAxisMode(
  728. options.yaxes[0],
  729. this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format,
  730. this.panel.yaxes[0].decimals
  731. );
  732. }
  733. parseNumber(value: any) {
  734. if (value === null || typeof value === 'undefined') {
  735. return null;
  736. }
  737. return toNumber(value);
  738. }
  739. applyLogScale(axis: any, data: any) {
  740. if (axis.logBase === 1) {
  741. return;
  742. }
  743. const minSetToZero = axis.min === 0;
  744. if (axis.min < Number.MIN_VALUE) {
  745. axis.min = null;
  746. }
  747. if (axis.max < Number.MIN_VALUE) {
  748. axis.max = null;
  749. }
  750. let series, i;
  751. let max = axis.max,
  752. min = axis.min;
  753. for (i = 0; i < data.length; i++) {
  754. series = data[i];
  755. if (series.yaxis === axis.index) {
  756. if (!max || max < series.stats.max) {
  757. max = series.stats.max;
  758. }
  759. if (!min || min > series.stats.logmin) {
  760. min = series.stats.logmin;
  761. }
  762. }
  763. }
  764. axis.transform = (v: number) => {
  765. return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);
  766. };
  767. axis.inverseTransform = (v: any) => {
  768. return Math.pow(axis.logBase, v);
  769. };
  770. if (!max && !min) {
  771. max = axis.inverseTransform(+2);
  772. min = axis.inverseTransform(-2);
  773. } else if (!max) {
  774. max = min * axis.inverseTransform(+4);
  775. } else if (!min) {
  776. min = max * axis.inverseTransform(-4);
  777. }
  778. if (axis.min) {
  779. min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
  780. } else {
  781. min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));
  782. }
  783. if (axis.max) {
  784. max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
  785. } else {
  786. max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));
  787. }
  788. if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
  789. return;
  790. }
  791. if (Number.isFinite(min) && Number.isFinite(max)) {
  792. if (minSetToZero) {
  793. axis.min = 0.1;
  794. min = 1;
  795. }
  796. axis.ticks = this.generateTicksForLogScaleYAxis(min, max, axis.logBase);
  797. if (minSetToZero) {
  798. axis.ticks.unshift(0.1);
  799. }
  800. if (axis.ticks[axis.ticks.length - 1] > axis.max) {
  801. axis.max = axis.ticks[axis.ticks.length - 1];
  802. }
  803. } else {
  804. axis.ticks = [1, 2];
  805. delete axis.min;
  806. delete axis.max;
  807. }
  808. }
  809. generateTicksForLogScaleYAxis(min: any, max: number, logBase: number) {
  810. let ticks = [];
  811. let nextTick;
  812. for (nextTick = min; nextTick <= max; nextTick *= logBase) {
  813. ticks.push(nextTick);
  814. }
  815. const maxNumTicks = Math.ceil(this.ctrl.height / 25);
  816. const numTicks = ticks.length;
  817. if (numTicks > maxNumTicks) {
  818. const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
  819. ticks = [];
  820. for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
  821. ticks.push(nextTick);
  822. }
  823. }
  824. return ticks;
  825. }
  826. configureAxisMode(
  827. axis: { tickFormatter: (val: any, axis: any) => string },
  828. format: string,
  829. decimals?: number | null
  830. ) {
  831. axis.tickFormatter = (val, axis) => {
  832. const formatter = getValueFormat(format);
  833. if (!formatter) {
  834. throw new Error(`Unit '${format}' is not supported`);
  835. }
  836. return formattedValueToString(formatter(val, decimals));
  837. };
  838. }
  839. }
  840. /** @ngInject */
  841. function graphDirective(timeSrv: TimeSrv, popoverSrv: any, contextSrv: ContextSrv) {
  842. return {
  843. restrict: 'A',
  844. template: '',
  845. link: (scope: any, elem: JQuery) => {
  846. return new GraphElement(scope, elem, timeSrv);
  847. },
  848. };
  849. }
  850. coreModule.directive('grafanaGraph', graphDirective);
  851. export { GraphElement, graphDirective };