LineGraphWidget.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. "use strict";
  2. const { Task } = require("devtools/shared/task");
  3. const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
  4. const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
  5. const { LocalizationHelper } = require("devtools/shared/l10n");
  6. const HTML_NS = "http://www.w3.org/1999/xhtml";
  7. const L10N = new LocalizationHelper("devtools/client/locales/graphs.properties");
  8. // Line graph constants.
  9. const GRAPH_DAMPEN_VALUES_FACTOR = 0.85;
  10. // px
  11. const GRAPH_TOOLTIP_SAFE_BOUNDS = 8;
  12. const GRAPH_MIN_MAX_TOOLTIP_DISTANCE = 14;
  13. const GRAPH_BACKGROUND_COLOR = "#0088cc";
  14. // px
  15. const GRAPH_STROKE_WIDTH = 1;
  16. const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
  17. // px
  18. const GRAPH_HELPER_LINES_DASH = [5];
  19. const GRAPH_HELPER_LINES_WIDTH = 1;
  20. const GRAPH_MAXIMUM_LINE_COLOR = "rgba(255,255,255,0.4)";
  21. const GRAPH_AVERAGE_LINE_COLOR = "rgba(255,255,255,0.7)";
  22. const GRAPH_MINIMUM_LINE_COLOR = "rgba(255,255,255,0.9)";
  23. const GRAPH_BACKGROUND_GRADIENT_START = "rgba(255,255,255,0.25)";
  24. const GRAPH_BACKGROUND_GRADIENT_END = "rgba(255,255,255,0.0)";
  25. const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
  26. const GRAPH_SELECTION_LINE_COLOR = "#fff";
  27. const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
  28. const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
  29. const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
  30. const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
  31. /**
  32. * A basic line graph, plotting values on a curve and adding helper lines
  33. * and tooltips for maximum, average and minimum values.
  34. *
  35. * @see AbstractCanvasGraph for emitted events and other options.
  36. *
  37. * Example usage:
  38. * let graph = new LineGraphWidget(node, "units");
  39. * graph.once("ready", () => {
  40. * graph.setData(src);
  41. * });
  42. *
  43. * Data source format:
  44. * [
  45. * { delta: x1, value: y1 },
  46. * { delta: x2, value: y2 },
  47. * ...
  48. * { delta: xn, value: yn }
  49. * ]
  50. * where each item in the array represents a point in the graph.
  51. *
  52. * @param nsIDOMNode parent
  53. * The parent node holding the graph.
  54. * @param object options [optional]
  55. * `metric`: The metric displayed in the graph, e.g. "fps" or "bananas".
  56. * `min`: Boolean whether to show the min tooltip/gutter/line (default: true)
  57. * `max`: Boolean whether to show the max tooltip/gutter/line (default: true)
  58. * `avg`: Boolean whether to show the avg tooltip/gutter/line (default: true)
  59. */
  60. this.LineGraphWidget = function (parent, options = {}, ...args) {
  61. let { metric, min, max, avg } = options;
  62. this._showMin = min !== false;
  63. this._showMax = max !== false;
  64. this._showAvg = avg !== false;
  65. AbstractCanvasGraph.apply(this, [parent, "line-graph", ...args]);
  66. this.once("ready", () => {
  67. // Create all gutters and tooltips incase the showing of min/max/avg
  68. // are changed later
  69. this._gutter = this._createGutter();
  70. this._maxGutterLine = this._createGutterLine("maximum");
  71. this._maxTooltip = this._createTooltip(
  72. "maximum", "start", L10N.getStr("graphs.label.maximum"), metric
  73. );
  74. this._minGutterLine = this._createGutterLine("minimum");
  75. this._minTooltip = this._createTooltip(
  76. "minimum", "start", L10N.getStr("graphs.label.minimum"), metric
  77. );
  78. this._avgGutterLine = this._createGutterLine("average");
  79. this._avgTooltip = this._createTooltip(
  80. "average", "end", L10N.getStr("graphs.label.average"), metric
  81. );
  82. });
  83. };
  84. LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
  85. backgroundColor: GRAPH_BACKGROUND_COLOR,
  86. backgroundGradientStart: GRAPH_BACKGROUND_GRADIENT_START,
  87. backgroundGradientEnd: GRAPH_BACKGROUND_GRADIENT_END,
  88. strokeColor: GRAPH_STROKE_COLOR,
  89. strokeWidth: GRAPH_STROKE_WIDTH,
  90. maximumLineColor: GRAPH_MAXIMUM_LINE_COLOR,
  91. averageLineColor: GRAPH_AVERAGE_LINE_COLOR,
  92. minimumLineColor: GRAPH_MINIMUM_LINE_COLOR,
  93. clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
  94. selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
  95. selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
  96. selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
  97. regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
  98. regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
  99. /**
  100. * Optionally offsets the `delta` in the data source by this scalar.
  101. */
  102. dataOffsetX: 0,
  103. /**
  104. * Optionally uses this value instead of the last tick in the data source
  105. * to compute the horizontal scaling.
  106. */
  107. dataDuration: 0,
  108. /**
  109. * The scalar used to multiply the graph values to leave some headroom.
  110. */
  111. dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
  112. /**
  113. * Specifies if min/max/avg tooltips have arrow handlers on their sides.
  114. */
  115. withTooltipArrows: true,
  116. /**
  117. * Specifies if min/max/avg tooltips are positioned based on the actual
  118. * values, or just placed next to the graph corners.
  119. */
  120. withFixedTooltipPositions: false,
  121. /**
  122. * Takes a list of numbers and plots them on a line graph representing
  123. * the rate of occurences in a specified interval. Useful for drawing
  124. * framerate, for example, from a sequence of timestamps.
  125. *
  126. * @param array timestamps
  127. * A list of numbers representing time, ordered ascending. For example,
  128. * this can be the raw data received from the framerate actor, which
  129. * represents the elapsed time on each refresh driver tick.
  130. * @param number interval
  131. * The maximum amount of time to wait between calculations.
  132. * @param number duration
  133. * The duration of the recording in milliseconds.
  134. */
  135. setDataFromTimestamps: Task.async(function* (timestamps, interval, duration) {
  136. let {
  137. plottedData,
  138. plottedMinMaxSum
  139. } = yield CanvasGraphUtils._performTaskInWorker("plotTimestampsGraph", {
  140. timestamps, interval, duration
  141. });
  142. this._tempMinMaxSum = plottedMinMaxSum;
  143. this.setData(plottedData);
  144. }),
  145. /**
  146. * Renders the graph's data source.
  147. * @see AbstractCanvasGraph.prototype.buildGraphImage
  148. */
  149. buildGraphImage: function () {
  150. let { canvas, ctx } = this._getNamedCanvas("line-graph-data");
  151. let width = this._width;
  152. let height = this._height;
  153. let totalTicks = this._data.length;
  154. let firstTick = totalTicks ? this._data[0].delta : 0;
  155. let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
  156. let maxValue = Number.MIN_SAFE_INTEGER;
  157. let minValue = Number.MAX_SAFE_INTEGER;
  158. let avgValue = 0;
  159. if (this._tempMinMaxSum) {
  160. maxValue = this._tempMinMaxSum.maxValue;
  161. minValue = this._tempMinMaxSum.minValue;
  162. avgValue = this._tempMinMaxSum.avgValue;
  163. } else {
  164. let sumValues = 0;
  165. for (let { value } of this._data) {
  166. maxValue = Math.max(value, maxValue);
  167. minValue = Math.min(value, minValue);
  168. sumValues += value;
  169. }
  170. avgValue = sumValues / totalTicks;
  171. }
  172. let duration = this.dataDuration || lastTick;
  173. let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
  174. let dataScaleY =
  175. this.dataScaleY = height / maxValue * this.dampenValuesFactor;
  176. // Draw the background.
  177. ctx.fillStyle = this.backgroundColor;
  178. ctx.fillRect(0, 0, width, height);
  179. // Draw the graph.
  180. let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
  181. gradient.addColorStop(0, this.backgroundGradientStart);
  182. gradient.addColorStop(1, this.backgroundGradientEnd);
  183. ctx.fillStyle = gradient;
  184. ctx.strokeStyle = this.strokeColor;
  185. ctx.lineWidth = this.strokeWidth * this._pixelRatio;
  186. ctx.beginPath();
  187. for (let { delta, value } of this._data) {
  188. let currX = (delta - this.dataOffsetX) * dataScaleX;
  189. let currY = height - value * dataScaleY;
  190. if (delta == firstTick) {
  191. ctx.moveTo(-GRAPH_STROKE_WIDTH, height);
  192. ctx.lineTo(-GRAPH_STROKE_WIDTH, currY);
  193. }
  194. ctx.lineTo(currX, currY);
  195. if (delta == lastTick) {
  196. ctx.lineTo(width + GRAPH_STROKE_WIDTH, currY);
  197. ctx.lineTo(width + GRAPH_STROKE_WIDTH, height);
  198. }
  199. }
  200. ctx.fill();
  201. ctx.stroke();
  202. this._drawOverlays(ctx, minValue, maxValue, avgValue, dataScaleY);
  203. return canvas;
  204. },
  205. /**
  206. * Draws the min, max and average horizontal lines, along with their
  207. * repsective tooltips.
  208. *
  209. * @param CanvasRenderingContext2D ctx
  210. * @param number minValue
  211. * @param number maxValue
  212. * @param number avgValue
  213. * @param number dataScaleY
  214. */
  215. _drawOverlays: function (ctx, minValue, maxValue, avgValue, dataScaleY) {
  216. let width = this._width;
  217. let height = this._height;
  218. let totalTicks = this._data.length;
  219. // Draw the maximum value horizontal line.
  220. if (this._showMax) {
  221. ctx.strokeStyle = this.maximumLineColor;
  222. ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
  223. ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
  224. ctx.beginPath();
  225. let maximumY = height - maxValue * dataScaleY;
  226. ctx.moveTo(0, maximumY);
  227. ctx.lineTo(width, maximumY);
  228. ctx.stroke();
  229. }
  230. // Draw the average value horizontal line.
  231. if (this._showAvg) {
  232. ctx.strokeStyle = this.averageLineColor;
  233. ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
  234. ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
  235. ctx.beginPath();
  236. let averageY = height - avgValue * dataScaleY;
  237. ctx.moveTo(0, averageY);
  238. ctx.lineTo(width, averageY);
  239. ctx.stroke();
  240. }
  241. // Draw the minimum value horizontal line.
  242. if (this._showMin) {
  243. ctx.strokeStyle = this.minimumLineColor;
  244. ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
  245. ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
  246. ctx.beginPath();
  247. let minimumY = height - minValue * dataScaleY;
  248. ctx.moveTo(0, minimumY);
  249. ctx.lineTo(width, minimumY);
  250. ctx.stroke();
  251. }
  252. // Update the tooltips text and gutter lines.
  253. this._maxTooltip.querySelector("[text=value]").textContent =
  254. L10N.numberWithDecimals(maxValue, 2);
  255. this._avgTooltip.querySelector("[text=value]").textContent =
  256. L10N.numberWithDecimals(avgValue, 2);
  257. this._minTooltip.querySelector("[text=value]").textContent =
  258. L10N.numberWithDecimals(minValue, 2);
  259. let bottom = height / this._pixelRatio;
  260. let maxPosY = CanvasGraphUtils.map(maxValue * this.dampenValuesFactor, 0,
  261. maxValue, bottom, 0);
  262. let avgPosY = CanvasGraphUtils.map(avgValue * this.dampenValuesFactor, 0,
  263. maxValue, bottom, 0);
  264. let minPosY = CanvasGraphUtils.map(minValue * this.dampenValuesFactor, 0,
  265. maxValue, bottom, 0);
  266. let safeTop = GRAPH_TOOLTIP_SAFE_BOUNDS;
  267. let safeBottom = bottom - GRAPH_TOOLTIP_SAFE_BOUNDS;
  268. let maxTooltipTop = (this.withFixedTooltipPositions
  269. ? safeTop : CanvasGraphUtils.clamp(maxPosY, safeTop, safeBottom));
  270. let avgTooltipTop = (this.withFixedTooltipPositions
  271. ? safeTop : CanvasGraphUtils.clamp(avgPosY, safeTop, safeBottom));
  272. let minTooltipTop = (this.withFixedTooltipPositions
  273. ? safeBottom : CanvasGraphUtils.clamp(minPosY, safeTop, safeBottom));
  274. this._maxTooltip.style.top = maxTooltipTop + "px";
  275. this._avgTooltip.style.top = avgTooltipTop + "px";
  276. this._minTooltip.style.top = minTooltipTop + "px";
  277. this._maxGutterLine.style.top = maxPosY + "px";
  278. this._avgGutterLine.style.top = avgPosY + "px";
  279. this._minGutterLine.style.top = minPosY + "px";
  280. this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
  281. this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
  282. this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
  283. let distanceMinMax = Math.abs(maxTooltipTop - minTooltipTop);
  284. this._maxTooltip.hidden = this._showMax === false
  285. || !totalTicks
  286. || distanceMinMax < GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
  287. this._avgTooltip.hidden = this._showAvg === false || !totalTicks;
  288. this._minTooltip.hidden = this._showMin === false || !totalTicks;
  289. this._gutter.hidden = (this._showMin === false &&
  290. this._showAvg === false &&
  291. this._showMax === false) || !totalTicks;
  292. this._maxGutterLine.hidden = this._showMax === false;
  293. this._avgGutterLine.hidden = this._showAvg === false;
  294. this._minGutterLine.hidden = this._showMin === false;
  295. },
  296. /**
  297. * Creates the gutter node when constructing this graph.
  298. * @return nsIDOMNode
  299. */
  300. _createGutter: function () {
  301. let gutter = this._document.createElementNS(HTML_NS, "div");
  302. gutter.className = "line-graph-widget-gutter";
  303. gutter.setAttribute("hidden", true);
  304. this._container.appendChild(gutter);
  305. return gutter;
  306. },
  307. /**
  308. * Creates the gutter line nodes when constructing this graph.
  309. * @return nsIDOMNode
  310. */
  311. _createGutterLine: function (type) {
  312. let line = this._document.createElementNS(HTML_NS, "div");
  313. line.className = "line-graph-widget-gutter-line";
  314. line.setAttribute("type", type);
  315. this._gutter.appendChild(line);
  316. return line;
  317. },
  318. /**
  319. * Creates the tooltip nodes when constructing this graph.
  320. * @return nsIDOMNode
  321. */
  322. _createTooltip: function (type, arrow, info, metric) {
  323. let tooltip = this._document.createElementNS(HTML_NS, "div");
  324. tooltip.className = "line-graph-widget-tooltip";
  325. tooltip.setAttribute("type", type);
  326. tooltip.setAttribute("arrow", arrow);
  327. tooltip.setAttribute("hidden", true);
  328. let infoNode = this._document.createElementNS(HTML_NS, "span");
  329. infoNode.textContent = info;
  330. infoNode.setAttribute("text", "info");
  331. let valueNode = this._document.createElementNS(HTML_NS, "span");
  332. valueNode.textContent = 0;
  333. valueNode.setAttribute("text", "value");
  334. let metricNode = this._document.createElementNS(HTML_NS, "span");
  335. metricNode.textContent = metric;
  336. metricNode.setAttribute("text", "metric");
  337. tooltip.appendChild(infoNode);
  338. tooltip.appendChild(valueNode);
  339. tooltip.appendChild(metricNode);
  340. this._container.appendChild(tooltip);
  341. return tooltip;
  342. }
  343. });
  344. module.exports = LineGraphWidget;