Graphs.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const { Task } = require("devtools/shared/task");
  6. const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
  7. const { getCurrentZoom } = require("devtools/shared/layout/utils");
  8. loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
  9. loader.lazyRequireGetter(this, "EventEmitter",
  10. "devtools/shared/event-emitter");
  11. loader.lazyImporter(this, "DevToolsWorker",
  12. "resource://devtools/shared/worker/worker.js");
  13. const HTML_NS = "http://www.w3.org/1999/xhtml";
  14. const GRAPH_SRC = "chrome://devtools/content/shared/widgets/graphs-frame.xhtml";
  15. const WORKER_URL =
  16. "resource://devtools/client/shared/widgets/GraphsWorker.js";
  17. // Generic constants.
  18. // ms
  19. const GRAPH_RESIZE_EVENTS_DRAIN = 100;
  20. const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075;
  21. const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.1;
  22. // px
  23. const GRAPH_WHEEL_MIN_SELECTION_WIDTH = 10;
  24. // px
  25. const GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH = 4;
  26. const GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD = 10;
  27. const GRAPH_MAX_SELECTION_LEFT_PADDING = 1;
  28. const GRAPH_MAX_SELECTION_RIGHT_PADDING = 1;
  29. // px
  30. const GRAPH_REGION_LINE_WIDTH = 1;
  31. const GRAPH_REGION_LINE_COLOR = "rgba(237,38,85,0.8)";
  32. // px
  33. const GRAPH_STRIPE_PATTERN_WIDTH = 16;
  34. const GRAPH_STRIPE_PATTERN_HEIGHT = 16;
  35. const GRAPH_STRIPE_PATTERN_LINE_WIDTH = 2;
  36. const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4;
  37. /**
  38. * Small data primitives for all graphs.
  39. */
  40. this.GraphCursor = function () {
  41. this.x = null;
  42. this.y = null;
  43. };
  44. this.GraphArea = function () {
  45. this.start = null;
  46. this.end = null;
  47. };
  48. this.GraphAreaDragger = function (anchor = new GraphArea()) {
  49. this.origin = null;
  50. this.anchor = anchor;
  51. };
  52. this.GraphAreaResizer = function () {
  53. this.margin = null;
  54. };
  55. /**
  56. * Base class for all graphs using a canvas to render the data source. Handles
  57. * frame creation, data source, selection bounds, cursor position, etc.
  58. *
  59. * Language:
  60. * - The "data" represents the values used when building the graph.
  61. * Its specific format is defined by the inheriting classes.
  62. *
  63. * - A "cursor" is the cliphead position across the X axis of the graph.
  64. *
  65. * - A "selection" is defined by a "start" and an "end" value and
  66. * represents the selected bounds in the graph.
  67. *
  68. * - A "region" is a highlighted area in the graph, also defined by a
  69. * "start" and an "end" value, but distinct from the "selection". It is
  70. * simply used to highlight important regions in the data.
  71. *
  72. * Instances of this class are EventEmitters with the following events:
  73. * - "ready": when the container iframe and canvas are created.
  74. * - "selecting": when the selection is set or changed.
  75. * - "deselecting": when the selection is dropped.
  76. *
  77. * @param nsIDOMNode parent
  78. * The parent node holding the graph.
  79. * @param string name
  80. * The graph type, used for setting the correct class names.
  81. * Currently supported: "line-graph" only.
  82. * @param number sharpness [optional]
  83. * Defaults to the current device pixel ratio.
  84. */
  85. this.AbstractCanvasGraph = function (parent, name, sharpness) {
  86. EventEmitter.decorate(this);
  87. this._parent = parent;
  88. this._ready = defer();
  89. this._uid = "canvas-graph-" + Date.now();
  90. this._renderTargets = new Map();
  91. AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
  92. this._iframe = iframe;
  93. this._window = iframe.contentWindow;
  94. this._topWindow = this._window.top;
  95. this._document = iframe.contentDocument;
  96. this._pixelRatio = sharpness || this._window.devicePixelRatio;
  97. let container =
  98. this._container = this._document.getElementById("graph-container");
  99. container.className = name + "-widget-container graph-widget-container";
  100. let canvas = this._canvas = this._document.getElementById("graph-canvas");
  101. canvas.className = name + "-widget-canvas graph-widget-canvas";
  102. let bounds = parent.getBoundingClientRect();
  103. bounds.width = this.fixedWidth || bounds.width;
  104. bounds.height = this.fixedHeight || bounds.height;
  105. iframe.setAttribute("width", bounds.width);
  106. iframe.setAttribute("height", bounds.height);
  107. this._width = canvas.width = bounds.width * this._pixelRatio;
  108. this._height = canvas.height = bounds.height * this._pixelRatio;
  109. this._ctx = canvas.getContext("2d");
  110. this._ctx.imageSmoothingEnabled = false;
  111. this._cursor = new GraphCursor();
  112. this._selection = new GraphArea();
  113. this._selectionDragger = new GraphAreaDragger();
  114. this._selectionResizer = new GraphAreaResizer();
  115. this._isMouseActive = false;
  116. this._onAnimationFrame = this._onAnimationFrame.bind(this);
  117. this._onMouseMove = this._onMouseMove.bind(this);
  118. this._onMouseDown = this._onMouseDown.bind(this);
  119. this._onMouseUp = this._onMouseUp.bind(this);
  120. this._onMouseWheel = this._onMouseWheel.bind(this);
  121. this._onMouseOut = this._onMouseOut.bind(this);
  122. this._onResize = this._onResize.bind(this);
  123. this.refresh = this.refresh.bind(this);
  124. this._window.addEventListener("mousemove", this._onMouseMove);
  125. this._window.addEventListener("mousedown", this._onMouseDown);
  126. this._window.addEventListener("MozMousePixelScroll", this._onMouseWheel);
  127. this._window.addEventListener("mouseout", this._onMouseOut);
  128. let ownerWindow = this._parent.ownerDocument.defaultView;
  129. ownerWindow.addEventListener("resize", this._onResize);
  130. this._animationId =
  131. this._window.requestAnimationFrame(this._onAnimationFrame);
  132. this._ready.resolve(this);
  133. this.emit("ready", this);
  134. });
  135. };
  136. AbstractCanvasGraph.prototype = {
  137. /**
  138. * Read-only width and height of the canvas.
  139. * @return number
  140. */
  141. get width() {
  142. return this._width;
  143. },
  144. get height() {
  145. return this._height;
  146. },
  147. /**
  148. * Return true if the mouse is actively messing with the selection, false
  149. * otherwise.
  150. */
  151. get isMouseActive() {
  152. return this._isMouseActive;
  153. },
  154. /**
  155. * Returns a promise resolved once this graph is ready to receive data.
  156. */
  157. ready: function () {
  158. return this._ready.promise;
  159. },
  160. /**
  161. * Destroys this graph.
  162. */
  163. destroy: Task.async(function* () {
  164. yield this.ready();
  165. this._topWindow.removeEventListener("mousemove", this._onMouseMove);
  166. this._topWindow.removeEventListener("mouseup", this._onMouseUp);
  167. this._window.removeEventListener("mousemove", this._onMouseMove);
  168. this._window.removeEventListener("mousedown", this._onMouseDown);
  169. this._window.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
  170. this._window.removeEventListener("mouseout", this._onMouseOut);
  171. let ownerWindow = this._parent.ownerDocument.defaultView;
  172. if (ownerWindow) {
  173. ownerWindow.removeEventListener("resize", this._onResize);
  174. }
  175. this._window.cancelAnimationFrame(this._animationId);
  176. this._iframe.remove();
  177. this._cursor = null;
  178. this._selection = null;
  179. this._selectionDragger = null;
  180. this._selectionResizer = null;
  181. this._data = null;
  182. this._mask = null;
  183. this._maskArgs = null;
  184. this._regions = null;
  185. this._cachedBackgroundImage = null;
  186. this._cachedGraphImage = null;
  187. this._cachedMaskImage = null;
  188. this._renderTargets.clear();
  189. gCachedStripePattern.clear();
  190. this.emit("destroyed");
  191. }),
  192. /**
  193. * Rendering options. Subclasses should override these.
  194. */
  195. clipheadLineWidth: 1,
  196. clipheadLineColor: "transparent",
  197. selectionLineWidth: 1,
  198. selectionLineColor: "transparent",
  199. selectionBackgroundColor: "transparent",
  200. selectionStripesColor: "transparent",
  201. regionBackgroundColor: "transparent",
  202. regionStripesColor: "transparent",
  203. /**
  204. * Makes sure the canvas graph is of the specified width or height, and
  205. * doesn't flex to fit all the available space.
  206. */
  207. fixedWidth: null,
  208. fixedHeight: null,
  209. /**
  210. * Optionally builds and caches a background image for this graph.
  211. * Inheriting classes may override this method.
  212. */
  213. buildBackgroundImage: function () {
  214. return null;
  215. },
  216. /**
  217. * Builds and caches a graph image, based on the data source supplied
  218. * in `setData`. The graph image is not rebuilt on each frame, but
  219. * only when the data source changes.
  220. */
  221. buildGraphImage: function () {
  222. let error = "This method needs to be implemented by inheriting classes.";
  223. throw new Error(error);
  224. },
  225. /**
  226. * Optionally builds and caches a mask image for this graph, composited
  227. * over the data image created via `buildGraphImage`. Inheriting classes
  228. * may override this method.
  229. */
  230. buildMaskImage: function () {
  231. return null;
  232. },
  233. /**
  234. * When setting the data source, the coordinates and values may be
  235. * stretched or squeezed on the X/Y axis, to fit into the available space.
  236. */
  237. dataScaleX: 1,
  238. dataScaleY: 1,
  239. /**
  240. * Sets the data source for this graph.
  241. *
  242. * @param object data
  243. * The data source. The actual format is specified by subclasses.
  244. */
  245. setData: function (data) {
  246. this._data = data;
  247. this._cachedBackgroundImage = this.buildBackgroundImage();
  248. this._cachedGraphImage = this.buildGraphImage();
  249. this._shouldRedraw = true;
  250. },
  251. /**
  252. * Same as `setData`, but waits for this graph to finish initializing first.
  253. *
  254. * @param object data
  255. * The data source. The actual format is specified by subclasses.
  256. * @return promise
  257. * A promise resolved once the data is set.
  258. */
  259. setDataWhenReady: Task.async(function* (data) {
  260. yield this.ready();
  261. this.setData(data);
  262. }),
  263. /**
  264. * Adds a mask to this graph.
  265. *
  266. * @param any mask, options
  267. * See `buildMaskImage` in inheriting classes for the required args.
  268. */
  269. setMask: function (mask, ...options) {
  270. this._mask = mask;
  271. this._maskArgs = [mask, ...options];
  272. this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs);
  273. this._shouldRedraw = true;
  274. },
  275. /**
  276. * Adds regions to this graph.
  277. *
  278. * See the "Language" section in the constructor documentation
  279. * for details about what "regions" represent.
  280. *
  281. * @param array regions
  282. * A list of { start, end } values.
  283. */
  284. setRegions: function (regions) {
  285. if (!this._cachedGraphImage) {
  286. throw new Error("Can't highlight regions on a graph with " +
  287. "no data displayed.");
  288. }
  289. if (this._regions) {
  290. throw new Error("Regions were already highlighted on the graph.");
  291. }
  292. this._regions = regions.map(e => ({
  293. start: e.start * this.dataScaleX,
  294. end: e.end * this.dataScaleX
  295. }));
  296. this._bakeRegions(this._regions, this._cachedGraphImage);
  297. this._shouldRedraw = true;
  298. },
  299. /**
  300. * Gets whether or not this graph has a data source.
  301. * @return boolean
  302. */
  303. hasData: function () {
  304. return !!this._data;
  305. },
  306. /**
  307. * Gets whether or not this graph has any mask applied.
  308. * @return boolean
  309. */
  310. hasMask: function () {
  311. return !!this._mask;
  312. },
  313. /**
  314. * Gets whether or not this graph has any regions.
  315. * @return boolean
  316. */
  317. hasRegions: function () {
  318. return !!this._regions;
  319. },
  320. /**
  321. * Sets the selection bounds.
  322. * Use `dropSelection` to remove the selection.
  323. *
  324. * If the bounds aren't different, no "selection" event is emitted.
  325. *
  326. * See the "Language" section in the constructor documentation
  327. * for details about what a "selection" represents.
  328. *
  329. * @param object selection
  330. * The selection's { start, end } values.
  331. */
  332. setSelection: function (selection) {
  333. if (!selection || selection.start == null || selection.end == null) {
  334. throw new Error("Invalid selection coordinates");
  335. }
  336. if (!this.isSelectionDifferent(selection)) {
  337. return;
  338. }
  339. this._selection.start = selection.start;
  340. this._selection.end = selection.end;
  341. this._shouldRedraw = true;
  342. this.emit("selecting");
  343. },
  344. /**
  345. * Gets the selection bounds.
  346. * If there's no selection, the bounds have null values.
  347. *
  348. * @return object
  349. * The selection's { start, end } values.
  350. */
  351. getSelection: function () {
  352. if (this.hasSelection()) {
  353. return { start: this._selection.start, end: this._selection.end };
  354. }
  355. if (this.hasSelectionInProgress()) {
  356. return { start: this._selection.start, end: this._cursor.x };
  357. }
  358. return { start: null, end: null };
  359. },
  360. /**
  361. * Sets the selection bounds, scaled to correlate with the data source ranges,
  362. * such that a [0, max width] selection maps to [first value, last value].
  363. *
  364. * @param object selection
  365. * The selection's { start, end } values.
  366. * @param object { mapStart, mapEnd } mapping [optional]
  367. * Invoked when retrieving the numbers in the data source representing
  368. * the first and last values, on the X axis.
  369. */
  370. setMappedSelection: function (selection, mapping = {}) {
  371. if (!this.hasData()) {
  372. throw new Error("A data source is necessary for retrieving " +
  373. "a mapped selection.");
  374. }
  375. if (!selection || selection.start == null || selection.end == null) {
  376. throw new Error("Invalid selection coordinates");
  377. }
  378. let { mapStart, mapEnd } = mapping;
  379. let startTime = (mapStart || (e => e.delta))(this._data[0]);
  380. let endTime = (mapEnd || (e => e.delta))(this._data[this._data.length - 1]);
  381. // The selection's start and end values are not guaranteed to be ascending.
  382. // Also make sure that the selection bounds fit inside the data bounds.
  383. let min = Math.max(Math.min(selection.start, selection.end), startTime);
  384. let max = Math.min(Math.max(selection.start, selection.end), endTime);
  385. min = map(min, startTime, endTime, 0, this._width);
  386. max = map(max, startTime, endTime, 0, this._width);
  387. this.setSelection({ start: min, end: max });
  388. },
  389. /**
  390. * Gets the selection bounds, scaled to correlate with the data source ranges,
  391. * such that a [0, max width] selection maps to [first value, last value].
  392. *
  393. * @param object { mapStart, mapEnd } mapping [optional]
  394. * Invoked when retrieving the numbers in the data source representing
  395. * the first and last values, on the X axis.
  396. * @return object
  397. * The mapped selection's { min, max } values.
  398. */
  399. getMappedSelection: function (mapping = {}) {
  400. if (!this.hasData()) {
  401. throw new Error("A data source is necessary for retrieving a " +
  402. "mapped selection.");
  403. }
  404. if (!this.hasSelection() && !this.hasSelectionInProgress()) {
  405. return { min: null, max: null };
  406. }
  407. let { mapStart, mapEnd } = mapping;
  408. let startTime = (mapStart || (e => e.delta))(this._data[0]);
  409. let endTime = (mapEnd || (e => e.delta))(this._data[this._data.length - 1]);
  410. // The selection's start and end values are not guaranteed to be ascending.
  411. // This can happen, for example, when click & dragging from right to left.
  412. // Also make sure that the selection bounds fit inside the canvas bounds.
  413. let selection = this.getSelection();
  414. let min = Math.max(Math.min(selection.start, selection.end), 0);
  415. let max = Math.min(Math.max(selection.start, selection.end), this._width);
  416. min = map(min, 0, this._width, startTime, endTime);
  417. max = map(max, 0, this._width, startTime, endTime);
  418. return { min: min, max: max };
  419. },
  420. /**
  421. * Removes the selection.
  422. */
  423. dropSelection: function () {
  424. if (!this.hasSelection() && !this.hasSelectionInProgress()) {
  425. return;
  426. }
  427. this._selection.start = null;
  428. this._selection.end = null;
  429. this._shouldRedraw = true;
  430. this.emit("deselecting");
  431. },
  432. /**
  433. * Gets whether or not this graph has a selection.
  434. * @return boolean
  435. */
  436. hasSelection: function () {
  437. return this._selection &&
  438. this._selection.start != null && this._selection.end != null;
  439. },
  440. /**
  441. * Gets whether or not a selection is currently being made, for example
  442. * via a click+drag operation.
  443. * @return boolean
  444. */
  445. hasSelectionInProgress: function () {
  446. return this._selection &&
  447. this._selection.start != null && this._selection.end == null;
  448. },
  449. /**
  450. * Specifies whether or not mouse selection is allowed.
  451. * @type boolean
  452. */
  453. selectionEnabled: true,
  454. /**
  455. * Sets the selection bounds.
  456. * Use `dropCursor` to hide the cursor.
  457. *
  458. * @param object cursor
  459. * The cursor's { x, y } position.
  460. */
  461. setCursor: function (cursor) {
  462. if (!cursor || cursor.x == null || cursor.y == null) {
  463. throw new Error("Invalid cursor coordinates");
  464. }
  465. if (!this.isCursorDifferent(cursor)) {
  466. return;
  467. }
  468. this._cursor.x = cursor.x;
  469. this._cursor.y = cursor.y;
  470. this._shouldRedraw = true;
  471. },
  472. /**
  473. * Gets the cursor position.
  474. * If there's no cursor, the position has null values.
  475. *
  476. * @return object
  477. * The cursor's { x, y } values.
  478. */
  479. getCursor: function () {
  480. return { x: this._cursor.x, y: this._cursor.y };
  481. },
  482. /**
  483. * Hides the cursor.
  484. */
  485. dropCursor: function () {
  486. if (!this.hasCursor()) {
  487. return;
  488. }
  489. this._cursor.x = null;
  490. this._cursor.y = null;
  491. this._shouldRedraw = true;
  492. },
  493. /**
  494. * Gets whether or not this graph has a visible cursor.
  495. * @return boolean
  496. */
  497. hasCursor: function () {
  498. return this._cursor && this._cursor.x != null;
  499. },
  500. /**
  501. * Specifies if this graph's selection is different from another one.
  502. *
  503. * @param object other
  504. * The other graph's selection, as { start, end } values.
  505. */
  506. isSelectionDifferent: function (other) {
  507. if (!other) {
  508. return true;
  509. }
  510. let current = this.getSelection();
  511. return current.start != other.start || current.end != other.end;
  512. },
  513. /**
  514. * Specifies if this graph's cursor is different from another one.
  515. *
  516. * @param object other
  517. * The other graph's position, as { x, y } values.
  518. */
  519. isCursorDifferent: function (other) {
  520. if (!other) {
  521. return true;
  522. }
  523. let current = this.getCursor();
  524. return current.x != other.x || current.y != other.y;
  525. },
  526. /**
  527. * Gets the width of the current selection.
  528. * If no selection is available, 0 is returned.
  529. *
  530. * @return number
  531. * The selection width.
  532. */
  533. getSelectionWidth: function () {
  534. let selection = this.getSelection();
  535. return Math.abs(selection.start - selection.end);
  536. },
  537. /**
  538. * Gets the currently hovered region, if any.
  539. * If no region is currently hovered, null is returned.
  540. *
  541. * @return object
  542. * The hovered region, as { start, end } values.
  543. */
  544. getHoveredRegion: function () {
  545. if (!this.hasRegions() || !this.hasCursor()) {
  546. return null;
  547. }
  548. let { x } = this._cursor;
  549. return this._regions.find(({ start, end }) =>
  550. (start < end && start < x && end > x) ||
  551. (start > end && end < x && start > x));
  552. },
  553. /**
  554. * Updates this graph to reflect the new dimensions of the parent node.
  555. *
  556. * @param boolean options.force
  557. * Force redrawing everything
  558. */
  559. refresh: function (options = {}) {
  560. let bounds = this._parent.getBoundingClientRect();
  561. let newWidth = this.fixedWidth || bounds.width;
  562. let newHeight = this.fixedHeight || bounds.height;
  563. // Prevent redrawing everything if the graph's width & height won't change,
  564. // except if force=true.
  565. if (!options.force &&
  566. this._width == newWidth * this._pixelRatio &&
  567. this._height == newHeight * this._pixelRatio) {
  568. this.emit("refresh-cancelled");
  569. return;
  570. }
  571. // Handle a changed size by mapping the old selection to the new width
  572. if (this._width && newWidth && this.hasSelection()) {
  573. let ratio = this._width / (newWidth * this._pixelRatio);
  574. this._selection.start = Math.round(this._selection.start / ratio);
  575. this._selection.end = Math.round(this._selection.end / ratio);
  576. }
  577. bounds.width = newWidth;
  578. bounds.height = newHeight;
  579. this._iframe.setAttribute("width", bounds.width);
  580. this._iframe.setAttribute("height", bounds.height);
  581. this._width = this._canvas.width = bounds.width * this._pixelRatio;
  582. this._height = this._canvas.height = bounds.height * this._pixelRatio;
  583. if (this.hasData()) {
  584. this._cachedBackgroundImage = this.buildBackgroundImage();
  585. this._cachedGraphImage = this.buildGraphImage();
  586. }
  587. if (this.hasMask()) {
  588. this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs);
  589. }
  590. if (this.hasRegions()) {
  591. this._bakeRegions(this._regions, this._cachedGraphImage);
  592. }
  593. this._shouldRedraw = true;
  594. this.emit("refresh");
  595. },
  596. /**
  597. * Gets a canvas with the specified name, for this graph.
  598. *
  599. * If it doesn't exist yet, it will be created, otherwise the cached instance
  600. * will be cleared and returned.
  601. *
  602. * @param string name
  603. * The canvas name.
  604. * @param number width, height [optional]
  605. * A custom width and height for the canvas. Defaults to this graph's
  606. * container canvas width and height.
  607. */
  608. _getNamedCanvas: function (name, width = this._width, height = this._height) {
  609. let cachedRenderTarget = this._renderTargets.get(name);
  610. if (cachedRenderTarget) {
  611. let { canvas, ctx } = cachedRenderTarget;
  612. canvas.width = width;
  613. canvas.height = height;
  614. ctx.clearRect(0, 0, width, height);
  615. return cachedRenderTarget;
  616. }
  617. let canvas = this._document.createElementNS(HTML_NS, "canvas");
  618. let ctx = canvas.getContext("2d");
  619. canvas.width = width;
  620. canvas.height = height;
  621. let renderTarget = { canvas: canvas, ctx: ctx };
  622. this._renderTargets.set(name, renderTarget);
  623. return renderTarget;
  624. },
  625. /**
  626. * The contents of this graph are redrawn only when something changed,
  627. * like the data source, or the selection bounds etc. This flag tracks
  628. * if the rendering is "dirty" and needs to be refreshed.
  629. */
  630. _shouldRedraw: false,
  631. /**
  632. * Animation frame callback, invoked on each tick of the refresh driver.
  633. */
  634. _onAnimationFrame: function () {
  635. this._animationId =
  636. this._window.requestAnimationFrame(this._onAnimationFrame);
  637. this._drawWidget();
  638. },
  639. /**
  640. * Redraws the widget when necessary. The actual graph is not refreshed
  641. * every time this function is called, only the cliphead, selection etc.
  642. */
  643. _drawWidget: function () {
  644. if (!this._shouldRedraw) {
  645. return;
  646. }
  647. let ctx = this._ctx;
  648. ctx.clearRect(0, 0, this._width, this._height);
  649. if (this._cachedGraphImage) {
  650. ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height);
  651. }
  652. if (this._cachedMaskImage) {
  653. ctx.globalCompositeOperation = "destination-out";
  654. ctx.drawImage(this._cachedMaskImage, 0, 0, this._width, this._height);
  655. }
  656. if (this._cachedBackgroundImage) {
  657. ctx.globalCompositeOperation = "destination-over";
  658. ctx.drawImage(this._cachedBackgroundImage, 0, 0,
  659. this._width, this._height);
  660. }
  661. // Revert to the original global composition operation.
  662. if (this._cachedMaskImage || this._cachedBackgroundImage) {
  663. ctx.globalCompositeOperation = "source-over";
  664. }
  665. if (this.hasCursor()) {
  666. this._drawCliphead();
  667. }
  668. if (this.hasSelection() || this.hasSelectionInProgress()) {
  669. this._drawSelection();
  670. }
  671. this._shouldRedraw = false;
  672. },
  673. /**
  674. * Draws the cliphead, if available and necessary.
  675. */
  676. _drawCliphead: function () {
  677. if (this._isHoveringSelectionContentsOrBoundaries() ||
  678. this._isHoveringRegion()) {
  679. return;
  680. }
  681. let ctx = this._ctx;
  682. ctx.lineWidth = this.clipheadLineWidth;
  683. ctx.strokeStyle = this.clipheadLineColor;
  684. ctx.beginPath();
  685. ctx.moveTo(this._cursor.x, 0);
  686. ctx.lineTo(this._cursor.x, this._height);
  687. ctx.stroke();
  688. },
  689. /**
  690. * Draws the selection, if available and necessary.
  691. */
  692. _drawSelection: function () {
  693. let { start, end } = this.getSelection();
  694. let input = this._canvas.getAttribute("input");
  695. let ctx = this._ctx;
  696. ctx.strokeStyle = this.selectionLineColor;
  697. // Fill selection.
  698. let pattern = AbstractCanvasGraph.getStripePattern({
  699. ownerDocument: this._document,
  700. backgroundColor: this.selectionBackgroundColor,
  701. stripesColor: this.selectionStripesColor
  702. });
  703. ctx.fillStyle = pattern;
  704. let rectStart = Math.min(this._width, Math.max(0, start));
  705. let rectEnd = Math.min(this._width, Math.max(0, end));
  706. ctx.fillRect(rectStart, 0, rectEnd - rectStart, this._height);
  707. // Draw left boundary.
  708. if (input == "hovering-selection-start-boundary") {
  709. ctx.lineWidth = GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH;
  710. } else {
  711. ctx.lineWidth = this.clipheadLineWidth;
  712. }
  713. ctx.beginPath();
  714. ctx.moveTo(start, 0);
  715. ctx.lineTo(start, this._height);
  716. ctx.stroke();
  717. // Draw right boundary.
  718. if (input == "hovering-selection-end-boundary") {
  719. ctx.lineWidth = GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH;
  720. } else {
  721. ctx.lineWidth = this.clipheadLineWidth;
  722. }
  723. ctx.beginPath();
  724. ctx.moveTo(end, this._height);
  725. ctx.lineTo(end, 0);
  726. ctx.stroke();
  727. },
  728. /**
  729. * Draws regions into the cached graph image, created via `buildGraphImage`.
  730. * Called when new regions are set.
  731. */
  732. _bakeRegions: function (regions, destination) {
  733. let ctx = destination.getContext("2d");
  734. let pattern = AbstractCanvasGraph.getStripePattern({
  735. ownerDocument: this._document,
  736. backgroundColor: this.regionBackgroundColor,
  737. stripesColor: this.regionStripesColor
  738. });
  739. ctx.fillStyle = pattern;
  740. ctx.strokeStyle = GRAPH_REGION_LINE_COLOR;
  741. ctx.lineWidth = GRAPH_REGION_LINE_WIDTH;
  742. let y = -GRAPH_REGION_LINE_WIDTH;
  743. let height = this._height + GRAPH_REGION_LINE_WIDTH;
  744. for (let { start, end } of regions) {
  745. let x = start;
  746. let width = end - start;
  747. ctx.fillRect(x, y, width, height);
  748. ctx.strokeRect(x, y, width, height);
  749. }
  750. },
  751. /**
  752. * Checks whether the start handle of the selection is hovered.
  753. * @return boolean
  754. */
  755. _isHoveringStartBoundary: function () {
  756. if (!this.hasSelection() || !this.hasCursor()) {
  757. return false;
  758. }
  759. let { x } = this._cursor;
  760. let { start } = this._selection;
  761. let threshold = GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD * this._pixelRatio;
  762. return Math.abs(start - x) < threshold;
  763. },
  764. /**
  765. * Checks whether the end handle of the selection is hovered.
  766. * @return boolean
  767. */
  768. _isHoveringEndBoundary: function () {
  769. if (!this.hasSelection() || !this.hasCursor()) {
  770. return false;
  771. }
  772. let { x } = this._cursor;
  773. let { end } = this._selection;
  774. let threshold = GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD * this._pixelRatio;
  775. return Math.abs(end - x) < threshold;
  776. },
  777. /**
  778. * Checks whether the selection is hovered.
  779. * @return boolean
  780. */
  781. _isHoveringSelectionContents: function () {
  782. if (!this.hasSelection() || !this.hasCursor()) {
  783. return false;
  784. }
  785. let { x } = this._cursor;
  786. let { start, end } = this._selection;
  787. return (start < end && start < x && end > x) ||
  788. (start > end && end < x && start > x);
  789. },
  790. /**
  791. * Checks whether the selection or its handles are hovered.
  792. * @return boolean
  793. */
  794. _isHoveringSelectionContentsOrBoundaries: function () {
  795. return this._isHoveringSelectionContents() ||
  796. this._isHoveringStartBoundary() ||
  797. this._isHoveringEndBoundary();
  798. },
  799. /**
  800. * Checks whether a region is hovered.
  801. * @return boolean
  802. */
  803. _isHoveringRegion: function () {
  804. return !!this.getHoveredRegion();
  805. },
  806. /**
  807. * Given a MouseEvent, make it relative to this._canvas.
  808. * @return object {mouseX,mouseY}
  809. */
  810. _getRelativeEventCoordinates: function (e) {
  811. // For ease of testing, testX and testY can be passed in as the event
  812. // object. If so, just return this.
  813. if ("testX" in e && "testY" in e) {
  814. return {
  815. mouseX: e.testX * this._pixelRatio,
  816. mouseY: e.testY * this._pixelRatio
  817. };
  818. }
  819. // This method is concerned with converting mouse event coordinates from
  820. // "screen space" to "local space" (in other words, relative to this
  821. // canvas's position, thus (0,0) would correspond to the upper left corner).
  822. // We can't simply use `clientX` and `clientY` because the given MouseEvent
  823. // object may be generated from events coming from other DOM nodes.
  824. // Therefore, we need to get a bounding box relative to the top document and
  825. // do some simple math to convert screen coords into local coords.
  826. // However, `getBoxQuads` may be a very costly operation depending on the
  827. // complexity of the "outside world" DOM, so cache the results until we
  828. // suspect they might change (e.g. on a resize).
  829. // It'd sure be nice if we could use `getBoundsWithoutFlushing`, but it's
  830. // not taking the document zoom factor into consideration consistently.
  831. if (!this._boundingBox || this._maybeDirtyBoundingBox) {
  832. let topDocument = this._topWindow.document;
  833. let boxQuad = this._canvas.getBoxQuads({ relativeTo: topDocument })[0];
  834. this._boundingBox = boxQuad;
  835. this._maybeDirtyBoundingBox = false;
  836. }
  837. let bb = this._boundingBox;
  838. let x = (e.screenX - this._topWindow.screenX) - bb.p1.x;
  839. let y = (e.screenY - this._topWindow.screenY) - bb.p1.y;
  840. // Don't allow the event coordinates to be bigger than the canvas
  841. // or less than 0.
  842. let maxX = bb.p2.x - bb.p1.x;
  843. let maxY = bb.p3.y - bb.p1.y;
  844. let mouseX = Math.max(0, Math.min(x, maxX)) * this._pixelRatio;
  845. let mouseY = Math.max(0, Math.min(y, maxY)) * this._pixelRatio;
  846. // The coordinates need to be modified with the current zoom level
  847. // to prevent them from being wrong.
  848. let zoom = getCurrentZoom(this._canvas);
  849. mouseX /= zoom;
  850. mouseY /= zoom;
  851. return {mouseX, mouseY};
  852. },
  853. /**
  854. * Listener for the "mousemove" event on the graph's container.
  855. */
  856. _onMouseMove: function (e) {
  857. let resizer = this._selectionResizer;
  858. let dragger = this._selectionDragger;
  859. // Need to stop propagation here, since this function can be bound
  860. // to both this._window and this._topWindow. It's only attached to
  861. // this._topWindow during a drag event. Null check here since tests
  862. // don't pass this method into the event object.
  863. if (e.stopPropagation && this._isMouseActive) {
  864. e.stopPropagation();
  865. }
  866. // If a mouseup happened outside the window and the current operation
  867. // is causing the selection to change, then end it.
  868. if (e.buttons == 0 && (this.hasSelectionInProgress() ||
  869. resizer.margin != null ||
  870. dragger.origin != null)) {
  871. this._onMouseUp();
  872. return;
  873. }
  874. let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
  875. this._cursor.x = mouseX;
  876. this._cursor.y = mouseY;
  877. if (resizer.margin != null) {
  878. this._selection[resizer.margin] = mouseX;
  879. this._shouldRedraw = true;
  880. this.emit("selecting");
  881. return;
  882. }
  883. if (dragger.origin != null) {
  884. this._selection.start = dragger.anchor.start - dragger.origin + mouseX;
  885. this._selection.end = dragger.anchor.end - dragger.origin + mouseX;
  886. this._shouldRedraw = true;
  887. this.emit("selecting");
  888. return;
  889. }
  890. if (this.hasSelectionInProgress()) {
  891. this._shouldRedraw = true;
  892. this.emit("selecting");
  893. return;
  894. }
  895. if (this.hasSelection()) {
  896. if (this._isHoveringStartBoundary()) {
  897. this._canvas.setAttribute("input", "hovering-selection-start-boundary");
  898. this._shouldRedraw = true;
  899. return;
  900. }
  901. if (this._isHoveringEndBoundary()) {
  902. this._canvas.setAttribute("input", "hovering-selection-end-boundary");
  903. this._shouldRedraw = true;
  904. return;
  905. }
  906. if (this._isHoveringSelectionContents()) {
  907. this._canvas.setAttribute("input", "hovering-selection-contents");
  908. this._shouldRedraw = true;
  909. return;
  910. }
  911. }
  912. let region = this.getHoveredRegion();
  913. if (region) {
  914. this._canvas.setAttribute("input", "hovering-region");
  915. } else {
  916. this._canvas.setAttribute("input", "hovering-background");
  917. }
  918. this._shouldRedraw = true;
  919. },
  920. /**
  921. * Listener for the "mousedown" event on the graph's container.
  922. */
  923. _onMouseDown: function (e) {
  924. this._isMouseActive = true;
  925. let {mouseX} = this._getRelativeEventCoordinates(e);
  926. switch (this._canvas.getAttribute("input")) {
  927. case "hovering-background":
  928. case "hovering-region":
  929. if (!this.selectionEnabled) {
  930. break;
  931. }
  932. this._selection.start = mouseX;
  933. this._selection.end = null;
  934. this.emit("selecting");
  935. break;
  936. case "hovering-selection-start-boundary":
  937. this._selectionResizer.margin = "start";
  938. break;
  939. case "hovering-selection-end-boundary":
  940. this._selectionResizer.margin = "end";
  941. break;
  942. case "hovering-selection-contents":
  943. this._selectionDragger.origin = mouseX;
  944. this._selectionDragger.anchor.start = this._selection.start;
  945. this._selectionDragger.anchor.end = this._selection.end;
  946. this._canvas.setAttribute("input", "dragging-selection-contents");
  947. break;
  948. }
  949. // During a drag, bind to the top level window so that mouse movement
  950. // outside of this frame will still work.
  951. this._topWindow.addEventListener("mousemove", this._onMouseMove);
  952. this._topWindow.addEventListener("mouseup", this._onMouseUp);
  953. this._shouldRedraw = true;
  954. this.emit("mousedown");
  955. },
  956. /**
  957. * Listener for the "mouseup" event on the graph's container.
  958. */
  959. _onMouseUp: function () {
  960. this._isMouseActive = false;
  961. switch (this._canvas.getAttribute("input")) {
  962. case "hovering-background":
  963. case "hovering-region":
  964. if (!this.selectionEnabled) {
  965. break;
  966. }
  967. if (this.getSelectionWidth() < 1) {
  968. let region = this.getHoveredRegion();
  969. if (region) {
  970. this._selection.start = region.start;
  971. this._selection.end = region.end;
  972. this.emit("selecting");
  973. } else {
  974. this._selection.start = null;
  975. this._selection.end = null;
  976. this.emit("deselecting");
  977. }
  978. } else {
  979. this._selection.end = this._cursor.x;
  980. this.emit("selecting");
  981. }
  982. break;
  983. case "hovering-selection-start-boundary":
  984. case "hovering-selection-end-boundary":
  985. this._selectionResizer.margin = null;
  986. break;
  987. case "dragging-selection-contents":
  988. this._selectionDragger.origin = null;
  989. this._canvas.setAttribute("input", "hovering-selection-contents");
  990. break;
  991. }
  992. // No longer dragging, no need to bind to the top level window.
  993. this._topWindow.removeEventListener("mousemove", this._onMouseMove);
  994. this._topWindow.removeEventListener("mouseup", this._onMouseUp);
  995. this._shouldRedraw = true;
  996. this.emit("mouseup");
  997. },
  998. /**
  999. * Listener for the "wheel" event on the graph's container.
  1000. */
  1001. _onMouseWheel: function (e) {
  1002. if (!this.hasSelection()) {
  1003. return;
  1004. }
  1005. let {mouseX} = this._getRelativeEventCoordinates(e);
  1006. let focusX = mouseX;
  1007. let selection = this._selection;
  1008. let vector = 0;
  1009. // If the selection is hovered, "zoom" towards or away the cursor,
  1010. // by shrinking or growing the selection.
  1011. if (this._isHoveringSelectionContentsOrBoundaries()) {
  1012. let distStart = selection.start - focusX;
  1013. let distEnd = selection.end - focusX;
  1014. vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY;
  1015. selection.start = selection.start + distStart * vector;
  1016. selection.end = selection.end + distEnd * vector;
  1017. } else {
  1018. // Otherwise, simply pan the selection towards the left or right.
  1019. let direction = 0;
  1020. if (focusX > selection.end) {
  1021. direction = Math.sign(focusX - selection.end);
  1022. } else if (focusX < selection.start) {
  1023. direction = Math.sign(focusX - selection.start);
  1024. }
  1025. vector = direction * e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY;
  1026. selection.start -= vector;
  1027. selection.end -= vector;
  1028. }
  1029. // Make sure the selection bounds are still comfortably inside the
  1030. // graph's bounds when zooming out, to keep the margin handles accessible.
  1031. let minStart = GRAPH_MAX_SELECTION_LEFT_PADDING;
  1032. let maxEnd = this._width - GRAPH_MAX_SELECTION_RIGHT_PADDING;
  1033. if (selection.start < minStart) {
  1034. selection.start = minStart;
  1035. }
  1036. if (selection.start > maxEnd) {
  1037. selection.start = maxEnd;
  1038. }
  1039. if (selection.end < minStart) {
  1040. selection.end = minStart;
  1041. }
  1042. if (selection.end > maxEnd) {
  1043. selection.end = maxEnd;
  1044. }
  1045. // Make sure the selection doesn't get too narrow when zooming in.
  1046. let thickness = Math.abs(selection.start - selection.end);
  1047. if (thickness < GRAPH_WHEEL_MIN_SELECTION_WIDTH) {
  1048. let midPoint = (selection.start + selection.end) / 2;
  1049. selection.start = midPoint - GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
  1050. selection.end = midPoint + GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
  1051. }
  1052. this._shouldRedraw = true;
  1053. this.emit("selecting");
  1054. this.emit("scroll");
  1055. },
  1056. /**
  1057. * Listener for the "mouseout" event on the graph's container.
  1058. * Clear any active cursors if a drag isn't happening.
  1059. */
  1060. _onMouseOut: function (e) {
  1061. if (!this._isMouseActive) {
  1062. this._cursor.x = null;
  1063. this._cursor.y = null;
  1064. this._canvas.removeAttribute("input");
  1065. this._shouldRedraw = true;
  1066. }
  1067. },
  1068. /**
  1069. * Listener for the "resize" event on the graph's parent node.
  1070. */
  1071. _onResize: function () {
  1072. if (this.hasData()) {
  1073. // The assumption is that resize events may change the outside world
  1074. // layout in a way that affects this graph's bounding box location
  1075. // relative to the top window's document. Graphs aren't currently
  1076. // (or ever) expected to move around on their own.
  1077. this._maybeDirtyBoundingBox = true;
  1078. setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
  1079. }
  1080. }
  1081. };
  1082. // Helper functions.
  1083. /**
  1084. * Creates an iframe element with the provided source URL, appends it to
  1085. * the specified node and invokes the callback once the content is loaded.
  1086. *
  1087. * @param string url
  1088. * The desired source URL for the iframe.
  1089. * @param nsIDOMNode parent
  1090. * The desired parent node for the iframe.
  1091. * @param function callback
  1092. * Invoked once the content is loaded, with the iframe as an argument.
  1093. */
  1094. AbstractCanvasGraph.createIframe = function (url, parent, callback) {
  1095. let iframe = parent.ownerDocument.createElementNS(HTML_NS, "iframe");
  1096. iframe.addEventListener("DOMContentLoaded", function onLoad() {
  1097. iframe.removeEventListener("DOMContentLoaded", onLoad);
  1098. callback(iframe);
  1099. });
  1100. // Setting 100% width on the frame and flex on the parent allows the graph
  1101. // to properly shrink when the window is resized to be smaller.
  1102. iframe.setAttribute("frameborder", "0");
  1103. iframe.style.width = "100%";
  1104. iframe.style.minWidth = "50px";
  1105. iframe.src = url;
  1106. parent.style.display = "flex";
  1107. parent.appendChild(iframe);
  1108. };
  1109. /**
  1110. * Gets a striped pattern used as a background in selections and regions.
  1111. *
  1112. * @param object data
  1113. * The following properties are required:
  1114. * - ownerDocument: the nsIDocumentElement owning the canvas
  1115. * - backgroundColor: a string representing the fill style
  1116. * - stripesColor: a string representing the stroke style
  1117. * @return nsIDOMCanvasPattern
  1118. * The custom striped pattern.
  1119. */
  1120. AbstractCanvasGraph.getStripePattern = function (data) {
  1121. let { ownerDocument, backgroundColor, stripesColor } = data;
  1122. let id = [backgroundColor, stripesColor].join(",");
  1123. if (gCachedStripePattern.has(id)) {
  1124. return gCachedStripePattern.get(id);
  1125. }
  1126. let canvas = ownerDocument.createElementNS(HTML_NS, "canvas");
  1127. let ctx = canvas.getContext("2d");
  1128. let width = canvas.width = GRAPH_STRIPE_PATTERN_WIDTH;
  1129. let height = canvas.height = GRAPH_STRIPE_PATTERN_HEIGHT;
  1130. ctx.fillStyle = backgroundColor;
  1131. ctx.fillRect(0, 0, width, height);
  1132. let pixelRatio = ownerDocument.defaultView.devicePixelRatio;
  1133. let scaledLineWidth = GRAPH_STRIPE_PATTERN_LINE_WIDTH * pixelRatio;
  1134. let scaledLineSpacing = GRAPH_STRIPE_PATTERN_LINE_SPACING * pixelRatio;
  1135. ctx.strokeStyle = stripesColor;
  1136. ctx.lineWidth = scaledLineWidth;
  1137. ctx.lineCap = "square";
  1138. ctx.beginPath();
  1139. for (let i = -height; i <= height; i += scaledLineSpacing) {
  1140. ctx.moveTo(width, i);
  1141. ctx.lineTo(0, i + height);
  1142. }
  1143. ctx.stroke();
  1144. let pattern = ctx.createPattern(canvas, "repeat");
  1145. gCachedStripePattern.set(id, pattern);
  1146. return pattern;
  1147. };
  1148. /**
  1149. * Cache used by `AbstractCanvasGraph.getStripePattern`.
  1150. */
  1151. const gCachedStripePattern = new Map();
  1152. /**
  1153. * Utility functions for graph canvases.
  1154. */
  1155. this.CanvasGraphUtils = {
  1156. _graphUtilsWorker: null,
  1157. _graphUtilsTaskId: 0,
  1158. /**
  1159. * Merges the animation loop of two graphs.
  1160. */
  1161. linkAnimation: Task.async(function* (graph1, graph2) {
  1162. if (!graph1 || !graph2) {
  1163. return;
  1164. }
  1165. yield graph1.ready();
  1166. yield graph2.ready();
  1167. let window = graph1._window;
  1168. window.cancelAnimationFrame(graph1._animationId);
  1169. window.cancelAnimationFrame(graph2._animationId);
  1170. let loop = () => {
  1171. window.requestAnimationFrame(loop);
  1172. graph1._drawWidget();
  1173. graph2._drawWidget();
  1174. };
  1175. window.requestAnimationFrame(loop);
  1176. }),
  1177. /**
  1178. * Makes sure selections in one graph are reflected in another.
  1179. */
  1180. linkSelection: function (graph1, graph2) {
  1181. if (!graph1 || !graph2) {
  1182. return;
  1183. }
  1184. if (graph1.hasSelection()) {
  1185. graph2.setSelection(graph1.getSelection());
  1186. } else {
  1187. graph2.dropSelection();
  1188. }
  1189. graph1.on("selecting", () => {
  1190. graph2.setSelection(graph1.getSelection());
  1191. });
  1192. graph2.on("selecting", () => {
  1193. graph1.setSelection(graph2.getSelection());
  1194. });
  1195. graph1.on("deselecting", () => {
  1196. graph2.dropSelection();
  1197. });
  1198. graph2.on("deselecting", () => {
  1199. graph1.dropSelection();
  1200. });
  1201. },
  1202. /**
  1203. * Performs the given task in a chrome worker, assuming it exists.
  1204. *
  1205. * @param string task
  1206. * The task name. Currently supported: "plotTimestampsGraph".
  1207. * @param any data
  1208. * Extra arguments to pass to the worker.
  1209. * @return object
  1210. * A promise that is resolved once the worker finishes the task.
  1211. */
  1212. _performTaskInWorker: function (task, data) {
  1213. let worker = this._graphUtilsWorker || new DevToolsWorker(WORKER_URL);
  1214. return worker.performTask(task, data);
  1215. }
  1216. };
  1217. /**
  1218. * Maps a value from one range to another.
  1219. * @param number value, istart, istop, ostart, ostop
  1220. * @return number
  1221. */
  1222. function map(value, istart, istop, ostart, ostop) {
  1223. let ratio = istop - istart;
  1224. if (ratio == 0) {
  1225. return value;
  1226. }
  1227. return ostart + (ostop - ostart) * ((value - istart) / ratio);
  1228. }
  1229. /**
  1230. * Constrains a value to a range.
  1231. * @param number value, min, max
  1232. * @return number
  1233. */
  1234. function clamp(value, min, max) {
  1235. if (value < min) {
  1236. return min;
  1237. }
  1238. if (value > max) {
  1239. return max;
  1240. }
  1241. return value;
  1242. }
  1243. exports.GraphCursor = GraphCursor;
  1244. exports.GraphArea = GraphArea;
  1245. exports.GraphAreaDragger = GraphAreaDragger;
  1246. exports.GraphAreaResizer = GraphAreaResizer;
  1247. exports.AbstractCanvasGraph = AbstractCanvasGraph;
  1248. exports.CanvasGraphUtils = CanvasGraphUtils;
  1249. exports.CanvasGraphUtils.map = map;
  1250. exports.CanvasGraphUtils.clamp = clamp;