module.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. import _ from "lodash";
  2. import $ from "jquery";
  3. import "vendor/flot/jquery.flot";
  4. import "vendor/flot/jquery.flot.gauge";
  5. import "app/features/panellinks/link_srv";
  6. import kbn from "app/core/utils/kbn";
  7. import config from "app/core/config";
  8. import TimeSeries from "app/core/time_series2";
  9. import { MetricsPanelCtrl } from "app/plugins/sdk";
  10. class SingleStatCtrl extends MetricsPanelCtrl {
  11. static templateUrl = "module.html";
  12. dataType = "timeseries";
  13. series: any[];
  14. data: any;
  15. fontSizes: any[];
  16. unitFormats: any[];
  17. invalidGaugeRange: boolean;
  18. panel: any;
  19. events: any;
  20. valueNameOptions: any[] = [
  21. { value: "min", text: "Min" },
  22. { value: "max", text: "Max" },
  23. { value: "avg", text: "Average" },
  24. { value: "current", text: "Current" },
  25. { value: "total", text: "Total" },
  26. { value: "name", text: "Name" },
  27. { value: "first", text: "First" },
  28. { value: "delta", text: "Delta" },
  29. { value: "diff", text: "Difference" },
  30. { value: "range", text: "Range" },
  31. { value: "last_time", text: "Time of last point" }
  32. ];
  33. tableColumnOptions: any;
  34. // Set and populate defaults
  35. panelDefaults = {
  36. links: [],
  37. datasource: null,
  38. maxDataPoints: 100,
  39. interval: null,
  40. targets: [{}],
  41. cacheTimeout: null,
  42. format: "none",
  43. prefix: "",
  44. postfix: "",
  45. nullText: null,
  46. valueMaps: [{ value: "null", op: "=", text: "N/A" }],
  47. mappingTypes: [
  48. { name: "value to text", value: 1 },
  49. { name: "range to text", value: 2 }
  50. ],
  51. rangeMaps: [{ from: "null", to: "null", text: "N/A" }],
  52. mappingType: 1,
  53. nullPointMode: "connected",
  54. valueName: "avg",
  55. prefixFontSize: "50%",
  56. valueFontSize: "80%",
  57. postfixFontSize: "50%",
  58. thresholds: "",
  59. colorBackground: false,
  60. colorValue: false,
  61. colors: ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
  62. sparkline: {
  63. show: false,
  64. full: false,
  65. lineColor: "rgb(31, 120, 193)",
  66. fillColor: "rgba(31, 118, 189, 0.18)"
  67. },
  68. gauge: {
  69. show: false,
  70. minValue: 0,
  71. maxValue: 100,
  72. thresholdMarkers: true,
  73. thresholdLabels: false
  74. },
  75. tableColumn: ""
  76. };
  77. /** @ngInject */
  78. constructor($scope, $injector, private $location, private linkSrv) {
  79. super($scope, $injector);
  80. _.defaults(this.panel, this.panelDefaults);
  81. this.events.on("data-received", this.onDataReceived.bind(this));
  82. this.events.on("data-error", this.onDataError.bind(this));
  83. this.events.on("data-snapshot-load", this.onDataReceived.bind(this));
  84. this.events.on("init-edit-mode", this.onInitEditMode.bind(this));
  85. this.onSparklineColorChange = this.onSparklineColorChange.bind(this);
  86. this.onSparklineFillChange = this.onSparklineFillChange.bind(this);
  87. }
  88. onInitEditMode() {
  89. this.fontSizes = [
  90. "20%",
  91. "30%",
  92. "50%",
  93. "70%",
  94. "80%",
  95. "100%",
  96. "110%",
  97. "120%",
  98. "150%",
  99. "170%",
  100. "200%"
  101. ];
  102. this.addEditorTab(
  103. "Options",
  104. "public/app/plugins/panel/singlestat/editor.html",
  105. 2
  106. );
  107. this.addEditorTab(
  108. "Value Mappings",
  109. "public/app/plugins/panel/singlestat/mappings.html",
  110. 3
  111. );
  112. this.unitFormats = kbn.getUnitFormats();
  113. }
  114. setUnitFormat(subItem) {
  115. this.panel.format = subItem.value;
  116. this.refresh();
  117. }
  118. onDataError(err) {
  119. this.onDataReceived([]);
  120. }
  121. onDataReceived(dataList) {
  122. const data: any = {};
  123. if (dataList.length > 0 && dataList[0].type === "table") {
  124. this.dataType = "table";
  125. const tableData = dataList.map(this.tableHandler.bind(this));
  126. this.setTableValues(tableData, data);
  127. } else {
  128. this.dataType = "timeseries";
  129. this.series = dataList.map(this.seriesHandler.bind(this));
  130. this.setValues(data);
  131. }
  132. this.data = data;
  133. this.render();
  134. }
  135. seriesHandler(seriesData) {
  136. var series = new TimeSeries({
  137. datapoints: seriesData.datapoints || [],
  138. alias: seriesData.target
  139. });
  140. series.flotpairs = series.getFlotPairs(this.panel.nullPointMode);
  141. return series;
  142. }
  143. tableHandler(tableData) {
  144. const datapoints = [];
  145. const columnNames = {};
  146. tableData.columns.forEach((column, columnIndex) => {
  147. columnNames[columnIndex] = column.text;
  148. });
  149. this.tableColumnOptions = columnNames;
  150. if (!_.find(tableData.columns, ["text", this.panel.tableColumn])) {
  151. this.setTableColumnToSensibleDefault(tableData);
  152. }
  153. tableData.rows.forEach(row => {
  154. const datapoint = {};
  155. row.forEach((value, columnIndex) => {
  156. const key = columnNames[columnIndex];
  157. datapoint[key] = value;
  158. });
  159. datapoints.push(datapoint);
  160. });
  161. return datapoints;
  162. }
  163. setTableColumnToSensibleDefault(tableData) {
  164. if (this.tableColumnOptions.length === 1) {
  165. this.panel.tableColumn = this.tableColumnOptions[0];
  166. } else {
  167. this.panel.tableColumn = _.find(tableData.columns, col => {
  168. return col.type !== "time";
  169. }).text;
  170. }
  171. }
  172. setTableValues(tableData, data) {
  173. if (!tableData || tableData.length === 0) {
  174. return;
  175. }
  176. if (
  177. tableData[0].length === 0 ||
  178. tableData[0][0][this.panel.tableColumn] === undefined
  179. ) {
  180. return;
  181. }
  182. const datapoint = tableData[0][0];
  183. data.value = datapoint[this.panel.tableColumn];
  184. if (_.isString(data.value)) {
  185. data.valueFormatted = _.escape(data.value);
  186. data.value = 0;
  187. data.valueRounded = 0;
  188. } else {
  189. const decimalInfo = this.getDecimalsForValue(data.value);
  190. const formatFunc = kbn.valueFormats[this.panel.format];
  191. data.valueFormatted = formatFunc(
  192. datapoint[this.panel.tableColumn],
  193. decimalInfo.decimals,
  194. decimalInfo.scaledDecimals
  195. );
  196. data.valueRounded = kbn.roundValue(data.value, this.panel.decimals || 0);
  197. }
  198. this.setValueMapping(data);
  199. }
  200. setColoring(options) {
  201. if (options.background) {
  202. this.panel.colorValue = false;
  203. this.panel.colors = [
  204. "rgba(71, 212, 59, 0.4)",
  205. "rgba(245, 150, 40, 0.73)",
  206. "rgba(225, 40, 40, 0.59)"
  207. ];
  208. } else {
  209. this.panel.colorBackground = false;
  210. this.panel.colors = [
  211. "rgba(50, 172, 45, 0.97)",
  212. "rgba(237, 129, 40, 0.89)",
  213. "rgba(245, 54, 54, 0.9)"
  214. ];
  215. }
  216. this.render();
  217. }
  218. invertColorOrder() {
  219. var tmp = this.panel.colors[0];
  220. this.panel.colors[0] = this.panel.colors[2];
  221. this.panel.colors[2] = tmp;
  222. this.render();
  223. }
  224. onColorChange(panelColorIndex) {
  225. return color => {
  226. this.panel.colors[panelColorIndex] = color;
  227. this.render();
  228. };
  229. }
  230. onSparklineColorChange(newColor) {
  231. this.panel.sparkline.lineColor = newColor;
  232. this.render();
  233. }
  234. onSparklineFillChange(newColor) {
  235. this.panel.sparkline.fillColor = newColor;
  236. this.render();
  237. }
  238. getDecimalsForValue(value) {
  239. if (_.isNumber(this.panel.decimals)) {
  240. return { decimals: this.panel.decimals, scaledDecimals: null };
  241. }
  242. var delta = value / 2;
  243. var dec = -Math.floor(Math.log(delta) / Math.LN10);
  244. var magn = Math.pow(10, -dec),
  245. norm = delta / magn, // norm is between 1.0 and 10.0
  246. size;
  247. if (norm < 1.5) {
  248. size = 1;
  249. } else if (norm < 3) {
  250. size = 2;
  251. // special case for 2.5, requires an extra decimal
  252. if (norm > 2.25) {
  253. size = 2.5;
  254. ++dec;
  255. }
  256. } else if (norm < 7.5) {
  257. size = 5;
  258. } else {
  259. size = 10;
  260. }
  261. size *= magn;
  262. // reduce starting decimals if not needed
  263. if (Math.floor(value) === value) {
  264. dec = 0;
  265. }
  266. var result: any = {};
  267. result.decimals = Math.max(0, dec);
  268. result.scaledDecimals =
  269. result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
  270. return result;
  271. }
  272. setValues(data) {
  273. data.flotpairs = [];
  274. if (this.series.length > 1) {
  275. var error: any = new Error();
  276. error.message = "Multiple Series Error";
  277. error.data =
  278. "Metric query returns " +
  279. this.series.length +
  280. " series. Single Stat Panel expects a single series.\n\nResponse:\n" +
  281. JSON.stringify(this.series);
  282. throw error;
  283. }
  284. if (this.series && this.series.length > 0) {
  285. let lastPoint = _.last(this.series[0].datapoints);
  286. let lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
  287. if (this.panel.valueName === "name") {
  288. data.value = 0;
  289. data.valueRounded = 0;
  290. data.valueFormatted = this.series[0].alias;
  291. } else if (_.isString(lastValue)) {
  292. data.value = 0;
  293. data.valueFormatted = _.escape(lastValue);
  294. data.valueRounded = 0;
  295. } else if (this.panel.valueName === "last_time") {
  296. let formatFunc = kbn.valueFormats[this.panel.format];
  297. data.value = lastPoint[1];
  298. data.valueRounded = data.value;
  299. data.valueFormatted = formatFunc(data.value, 0, 0);
  300. } else {
  301. data.value = this.series[0].stats[this.panel.valueName];
  302. data.flotpairs = this.series[0].flotpairs;
  303. let decimalInfo = this.getDecimalsForValue(data.value);
  304. let formatFunc = kbn.valueFormats[this.panel.format];
  305. data.valueFormatted = formatFunc(
  306. data.value,
  307. decimalInfo.decimals,
  308. decimalInfo.scaledDecimals
  309. );
  310. data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
  311. }
  312. // Add $__name variable for using in prefix or postfix
  313. data.scopedVars = _.extend({}, this.panel.scopedVars);
  314. data.scopedVars["__name"] = { value: this.series[0].label };
  315. }
  316. this.setValueMapping(data);
  317. }
  318. setValueMapping(data) {
  319. // check value to text mappings if its enabled
  320. if (this.panel.mappingType === 1) {
  321. for (let i = 0; i < this.panel.valueMaps.length; i++) {
  322. let map = this.panel.valueMaps[i];
  323. // special null case
  324. if (map.value === "null") {
  325. if (data.value === null || data.value === void 0) {
  326. data.valueFormatted = map.text;
  327. return;
  328. }
  329. continue;
  330. }
  331. // value/number to text mapping
  332. var value = parseFloat(map.value);
  333. if (value === data.valueRounded) {
  334. data.valueFormatted = map.text;
  335. return;
  336. }
  337. }
  338. } else if (this.panel.mappingType === 2) {
  339. for (let i = 0; i < this.panel.rangeMaps.length; i++) {
  340. let map = this.panel.rangeMaps[i];
  341. // special null case
  342. if (map.from === "null" && map.to === "null") {
  343. if (data.value === null || data.value === void 0) {
  344. data.valueFormatted = map.text;
  345. return;
  346. }
  347. continue;
  348. }
  349. // value/number to range mapping
  350. var from = parseFloat(map.from);
  351. var to = parseFloat(map.to);
  352. if (to >= data.valueRounded && from <= data.valueRounded) {
  353. data.valueFormatted = map.text;
  354. return;
  355. }
  356. }
  357. }
  358. if (data.value === null || data.value === void 0) {
  359. data.valueFormatted = "no value";
  360. }
  361. }
  362. removeValueMap(map) {
  363. var index = _.indexOf(this.panel.valueMaps, map);
  364. this.panel.valueMaps.splice(index, 1);
  365. this.render();
  366. }
  367. addValueMap() {
  368. this.panel.valueMaps.push({ value: "", op: "=", text: "" });
  369. }
  370. removeRangeMap(rangeMap) {
  371. var index = _.indexOf(this.panel.rangeMaps, rangeMap);
  372. this.panel.rangeMaps.splice(index, 1);
  373. this.render();
  374. }
  375. addRangeMap() {
  376. this.panel.rangeMaps.push({ from: "", to: "", text: "" });
  377. }
  378. link(scope, elem, attrs, ctrl) {
  379. var $location = this.$location;
  380. var linkSrv = this.linkSrv;
  381. var $timeout = this.$timeout;
  382. var panel = ctrl.panel;
  383. var templateSrv = this.templateSrv;
  384. var data, linkInfo;
  385. var $panelContainer = elem.find(".panel-container");
  386. elem = elem.find(".singlestat-panel");
  387. function applyColoringThresholds(value, valueString) {
  388. if (!panel.colorValue) {
  389. return valueString;
  390. }
  391. var color = getColorForValue(data, value);
  392. if (color) {
  393. return '<span style="color:' + color + '">' + valueString + "</span>";
  394. }
  395. return valueString;
  396. }
  397. function getSpan(className, fontSize, value) {
  398. value = templateSrv.replace(value, data.scopedVars);
  399. return (
  400. '<span class="' +
  401. className +
  402. '" style="font-size:' +
  403. fontSize +
  404. '">' +
  405. value +
  406. "</span>"
  407. );
  408. }
  409. function getBigValueHtml() {
  410. var body = '<div class="singlestat-panel-value-container">';
  411. if (panel.prefix) {
  412. body += getSpan(
  413. "singlestat-panel-prefix",
  414. panel.prefixFontSize,
  415. panel.prefix
  416. );
  417. }
  418. var value = applyColoringThresholds(data.value, data.valueFormatted);
  419. body += getSpan("singlestat-panel-value", panel.valueFontSize, value);
  420. if (panel.postfix) {
  421. body += getSpan(
  422. "singlestat-panel-postfix",
  423. panel.postfixFontSize,
  424. panel.postfix
  425. );
  426. }
  427. body += "</div>";
  428. return body;
  429. }
  430. function getValueText() {
  431. var result = panel.prefix
  432. ? templateSrv.replace(panel.prefix, data.scopedVars)
  433. : "";
  434. result += data.valueFormatted;
  435. result += panel.postfix
  436. ? templateSrv.replace(panel.postfix, data.scopedVars)
  437. : "";
  438. return result;
  439. }
  440. function addGauge() {
  441. var width = elem.width();
  442. var height = elem.height();
  443. // Allow to use a bit more space for wide gauges
  444. var dimension = Math.min(width, height * 1.3);
  445. ctrl.invalidGaugeRange = false;
  446. if (panel.gauge.minValue > panel.gauge.maxValue) {
  447. ctrl.invalidGaugeRange = true;
  448. return;
  449. }
  450. var plotCanvas = $("<div></div>");
  451. var plotCss = {
  452. top: "10px",
  453. margin: "auto",
  454. position: "relative",
  455. height: height * 0.9 + "px",
  456. width: dimension + "px"
  457. };
  458. plotCanvas.css(plotCss);
  459. var thresholds = [];
  460. for (var i = 0; i < data.thresholds.length; i++) {
  461. thresholds.push({
  462. value: data.thresholds[i],
  463. color: data.colorMap[i]
  464. });
  465. }
  466. thresholds.push({
  467. value: panel.gauge.maxValue,
  468. color: data.colorMap[data.colorMap.length - 1]
  469. });
  470. var bgColor = config.bootData.user.lightTheme
  471. ? "rgb(230,230,230)"
  472. : "rgb(38,38,38)";
  473. var fontScale = parseInt(panel.valueFontSize) / 100;
  474. var fontSize = Math.min(dimension / 5, 100) * fontScale;
  475. // Reduce gauge width if threshold labels enabled
  476. var gaugeWidthReduceRatio = panel.gauge.thresholdLabels ? 1.5 : 1;
  477. var gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
  478. var thresholdMarkersWidth = gaugeWidth / 5;
  479. var thresholdLabelFontSize = fontSize / 2.5;
  480. var options = {
  481. series: {
  482. gauges: {
  483. gauge: {
  484. min: panel.gauge.minValue,
  485. max: panel.gauge.maxValue,
  486. background: { color: bgColor },
  487. border: { color: null },
  488. shadow: { show: false },
  489. width: gaugeWidth
  490. },
  491. frame: { show: false },
  492. label: { show: false },
  493. layout: { margin: 0, thresholdWidth: 0 },
  494. cell: { border: { width: 0 } },
  495. threshold: {
  496. values: thresholds,
  497. label: {
  498. show: panel.gauge.thresholdLabels,
  499. margin: thresholdMarkersWidth + 1,
  500. font: { size: thresholdLabelFontSize }
  501. },
  502. show: panel.gauge.thresholdMarkers,
  503. width: thresholdMarkersWidth
  504. },
  505. value: {
  506. color: panel.colorValue
  507. ? getColorForValue(data, data.valueRounded)
  508. : null,
  509. formatter: function() {
  510. return getValueText();
  511. },
  512. font: {
  513. size: fontSize,
  514. family: '"Helvetica Neue", Helvetica, Arial, sans-serif'
  515. }
  516. },
  517. show: true
  518. }
  519. }
  520. };
  521. elem.append(plotCanvas);
  522. var plotSeries = {
  523. data: [[0, data.valueRounded]]
  524. };
  525. $.plot(plotCanvas, [plotSeries], options);
  526. }
  527. function addSparkline() {
  528. var width = elem.width() + 20;
  529. if (width < 30) {
  530. // element has not gotten it's width yet
  531. // delay sparkline render
  532. setTimeout(addSparkline, 30);
  533. return;
  534. }
  535. var height = ctrl.height;
  536. var plotCanvas = $("<div></div>");
  537. var plotCss: any = {};
  538. plotCss.position = "absolute";
  539. if (panel.sparkline.full) {
  540. plotCss.bottom = "5px";
  541. plotCss.left = "-5px";
  542. plotCss.width = width - 10 + "px";
  543. var dynamicHeightMargin =
  544. height <= 100 ? 5 : Math.round(height / 100) * 15 + 5;
  545. plotCss.height = height - dynamicHeightMargin + "px";
  546. } else {
  547. plotCss.bottom = "0px";
  548. plotCss.left = "-5px";
  549. plotCss.width = width - 10 + "px";
  550. plotCss.height = Math.floor(height * 0.25) + "px";
  551. }
  552. plotCanvas.css(plotCss);
  553. var options = {
  554. legend: { show: false },
  555. series: {
  556. lines: {
  557. show: true,
  558. fill: 1,
  559. lineWidth: 1,
  560. fillColor: panel.sparkline.fillColor
  561. }
  562. },
  563. yaxes: { show: false },
  564. xaxis: {
  565. show: false,
  566. mode: "time",
  567. min: ctrl.range.from.valueOf(),
  568. max: ctrl.range.to.valueOf()
  569. },
  570. grid: { hoverable: false, show: false }
  571. };
  572. elem.append(plotCanvas);
  573. var plotSeries = {
  574. data: data.flotpairs,
  575. color: panel.sparkline.lineColor
  576. };
  577. $.plot(plotCanvas, [plotSeries], options);
  578. }
  579. function render() {
  580. if (!ctrl.data) {
  581. return;
  582. }
  583. data = ctrl.data;
  584. // get thresholds
  585. data.thresholds = panel.thresholds.split(",").map(function(strVale) {
  586. return Number(strVale.trim());
  587. });
  588. data.colorMap = panel.colors;
  589. var body = panel.gauge.show ? "" : getBigValueHtml();
  590. if (panel.colorBackground) {
  591. var color = getColorForValue(data, data.value);
  592. if (color) {
  593. $panelContainer.css("background-color", color);
  594. if (scope.fullscreen) {
  595. elem.css("background-color", color);
  596. } else {
  597. elem.css("background-color", "");
  598. }
  599. }
  600. } else {
  601. $panelContainer.css("background-color", "");
  602. elem.css("background-color", "");
  603. }
  604. elem.html(body);
  605. if (panel.sparkline.show) {
  606. addSparkline();
  607. }
  608. if (panel.gauge.show) {
  609. addGauge();
  610. }
  611. elem.toggleClass("pointer", panel.links.length > 0);
  612. if (panel.links.length > 0) {
  613. linkInfo = linkSrv.getPanelLinkAnchorInfo(
  614. panel.links[0],
  615. data.scopedVars
  616. );
  617. } else {
  618. linkInfo = null;
  619. }
  620. }
  621. function hookupDrilldownLinkTooltip() {
  622. // drilldown link tooltip
  623. var drilldownTooltip = $('<div id="tooltip" class="">hello</div>"');
  624. elem.mouseleave(function() {
  625. if (panel.links.length === 0) {
  626. return;
  627. }
  628. $timeout(function() {
  629. drilldownTooltip.detach();
  630. });
  631. });
  632. elem.click(function(evt) {
  633. if (!linkInfo) {
  634. return;
  635. }
  636. // ignore title clicks in title
  637. if ($(evt).parents(".panel-header").length > 0) {
  638. return;
  639. }
  640. if (linkInfo.target === "_blank") {
  641. window.open(linkInfo.href, "_blank");
  642. return;
  643. }
  644. if (linkInfo.href.indexOf("http") === 0) {
  645. window.location.href = linkInfo.href;
  646. } else {
  647. $timeout(function() {
  648. $location.url(linkInfo.href);
  649. });
  650. }
  651. drilldownTooltip.detach();
  652. });
  653. elem.mousemove(function(e) {
  654. if (!linkInfo) {
  655. return;
  656. }
  657. drilldownTooltip.text("click to go to: " + linkInfo.title);
  658. drilldownTooltip.place_tt(e.pageX, e.pageY - 50);
  659. });
  660. }
  661. hookupDrilldownLinkTooltip();
  662. this.events.on("render", function() {
  663. render();
  664. ctrl.renderingCompleted();
  665. });
  666. }
  667. }
  668. function getColorForValue(data, value) {
  669. if (!_.isFinite(value)) {
  670. return null;
  671. }
  672. for (var i = data.thresholds.length; i > 0; i--) {
  673. if (value >= data.thresholds[i - 1]) {
  674. return data.colorMap[i];
  675. }
  676. }
  677. return _.first(data.colorMap);
  678. }
  679. export { SingleStatCtrl, SingleStatCtrl as PanelCtrl, getColorForValue };