mpl.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. /* Put everything inside the global mpl namespace */
  2. window.mpl = {};
  3. mpl.get_websocket_type = function() {
  4. if (typeof(WebSocket) !== 'undefined') {
  5. return WebSocket;
  6. } else if (typeof(MozWebSocket) !== 'undefined') {
  7. return MozWebSocket;
  8. } else {
  9. alert('Your browser does not have WebSocket support. ' +
  10. 'Please try Chrome, Safari or Firefox ≥ 6. ' +
  11. 'Firefox 4 and 5 are also supported but you ' +
  12. 'have to enable WebSockets in about:config.');
  13. };
  14. }
  15. mpl.figure = function(figure_id, websocket, ondownload, parent_element) {
  16. this.id = figure_id;
  17. this.ws = websocket;
  18. this.supports_binary = (this.ws.binaryType != undefined);
  19. if (!this.supports_binary) {
  20. var warnings = document.getElementById("mpl-warnings");
  21. if (warnings) {
  22. warnings.style.display = 'block';
  23. warnings.textContent = (
  24. "This browser does not support binary websocket messages. " +
  25. "Performance may be slow.");
  26. }
  27. }
  28. this.imageObj = new Image();
  29. this.context = undefined;
  30. this.message = undefined;
  31. this.canvas = undefined;
  32. this.rubberband_canvas = undefined;
  33. this.rubberband_context = undefined;
  34. this.format_dropdown = undefined;
  35. this.image_mode = 'full';
  36. this.root = $('<div/>');
  37. this._root_extra_style(this.root)
  38. this.root.attr('style', 'display: inline-block');
  39. $(parent_element).append(this.root);
  40. this._init_header(this);
  41. this._init_canvas(this);
  42. this._init_toolbar(this);
  43. var fig = this;
  44. this.waiting = false;
  45. this.ws.onopen = function () {
  46. fig.send_message("supports_binary", {value: fig.supports_binary});
  47. fig.send_message("send_image_mode", {});
  48. if (mpl.ratio != 1) {
  49. fig.send_message("set_dpi_ratio", {'dpi_ratio': mpl.ratio});
  50. }
  51. fig.send_message("refresh", {});
  52. }
  53. this.imageObj.onload = function() {
  54. if (fig.image_mode == 'full') {
  55. // Full images could contain transparency (where diff images
  56. // almost always do), so we need to clear the canvas so that
  57. // there is no ghosting.
  58. fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);
  59. }
  60. fig.context.drawImage(fig.imageObj, 0, 0);
  61. };
  62. this.imageObj.onunload = function() {
  63. fig.ws.close();
  64. }
  65. this.ws.onmessage = this._make_on_message_function(this);
  66. this.ondownload = ondownload;
  67. }
  68. mpl.figure.prototype._init_header = function() {
  69. var titlebar = $(
  70. '<div class="ui-dialog-titlebar ui-widget-header ui-corner-all ' +
  71. 'ui-helper-clearfix"/>');
  72. var titletext = $(
  73. '<div class="ui-dialog-title" style="width: 100%; ' +
  74. 'text-align: center; padding: 3px;"/>');
  75. titlebar.append(titletext)
  76. this.root.append(titlebar);
  77. this.header = titletext[0];
  78. }
  79. mpl.figure.prototype._canvas_extra_style = function(canvas_div) {
  80. }
  81. mpl.figure.prototype._root_extra_style = function(canvas_div) {
  82. }
  83. mpl.figure.prototype._init_canvas = function() {
  84. var fig = this;
  85. var canvas_div = $('<div/>');
  86. canvas_div.attr('style', 'position: relative; clear: both; outline: 0');
  87. function canvas_keyboard_event(event) {
  88. return fig.key_event(event, event['data']);
  89. }
  90. canvas_div.keydown('key_press', canvas_keyboard_event);
  91. canvas_div.keyup('key_release', canvas_keyboard_event);
  92. this.canvas_div = canvas_div
  93. this._canvas_extra_style(canvas_div)
  94. this.root.append(canvas_div);
  95. var canvas = $('<canvas/>');
  96. canvas.addClass('mpl-canvas');
  97. canvas.attr('style', "left: 0; top: 0; z-index: 0; outline: 0")
  98. this.canvas = canvas[0];
  99. this.context = canvas[0].getContext("2d");
  100. var backingStore = this.context.backingStorePixelRatio ||
  101. this.context.webkitBackingStorePixelRatio ||
  102. this.context.mozBackingStorePixelRatio ||
  103. this.context.msBackingStorePixelRatio ||
  104. this.context.oBackingStorePixelRatio ||
  105. this.context.backingStorePixelRatio || 1;
  106. mpl.ratio = (window.devicePixelRatio || 1) / backingStore;
  107. var rubberband = $('<canvas/>');
  108. rubberband.attr('style', "position: absolute; left: 0; top: 0; z-index: 1;")
  109. var pass_mouse_events = true;
  110. canvas_div.resizable({
  111. start: function(event, ui) {
  112. pass_mouse_events = false;
  113. },
  114. resize: function(event, ui) {
  115. fig.request_resize(ui.size.width, ui.size.height);
  116. },
  117. stop: function(event, ui) {
  118. pass_mouse_events = true;
  119. fig.request_resize(ui.size.width, ui.size.height);
  120. },
  121. });
  122. function mouse_event_fn(event) {
  123. if (pass_mouse_events)
  124. return fig.mouse_event(event, event['data']);
  125. }
  126. rubberband.mousedown('button_press', mouse_event_fn);
  127. rubberband.mouseup('button_release', mouse_event_fn);
  128. // Throttle sequential mouse events to 1 every 20ms.
  129. rubberband.mousemove('motion_notify', mouse_event_fn);
  130. rubberband.mouseenter('figure_enter', mouse_event_fn);
  131. rubberband.mouseleave('figure_leave', mouse_event_fn);
  132. canvas_div.on("wheel", function (event) {
  133. event = event.originalEvent;
  134. event['data'] = 'scroll'
  135. if (event.deltaY < 0) {
  136. event.step = 1;
  137. } else {
  138. event.step = -1;
  139. }
  140. mouse_event_fn(event);
  141. });
  142. canvas_div.append(canvas);
  143. canvas_div.append(rubberband);
  144. this.rubberband = rubberband;
  145. this.rubberband_canvas = rubberband[0];
  146. this.rubberband_context = rubberband[0].getContext("2d");
  147. this.rubberband_context.strokeStyle = "#000000";
  148. this._resize_canvas = function(width, height) {
  149. // Keep the size of the canvas, canvas container, and rubber band
  150. // canvas in synch.
  151. canvas_div.css('width', width)
  152. canvas_div.css('height', height)
  153. canvas.attr('width', width * mpl.ratio);
  154. canvas.attr('height', height * mpl.ratio);
  155. canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');
  156. rubberband.attr('width', width);
  157. rubberband.attr('height', height);
  158. }
  159. // Set the figure to an initial 600x600px, this will subsequently be updated
  160. // upon first draw.
  161. this._resize_canvas(600, 600);
  162. // Disable right mouse context menu.
  163. $(this.rubberband_canvas).bind("contextmenu",function(e){
  164. return false;
  165. });
  166. function set_focus () {
  167. canvas.focus();
  168. canvas_div.focus();
  169. }
  170. window.setTimeout(set_focus, 100);
  171. }
  172. mpl.figure.prototype._init_toolbar = function() {
  173. var fig = this;
  174. var nav_element = $('<div/>');
  175. nav_element.attr('style', 'width: 100%');
  176. this.root.append(nav_element);
  177. // Define a callback function for later on.
  178. function toolbar_event(event) {
  179. return fig.toolbar_button_onclick(event['data']);
  180. }
  181. function toolbar_mouse_event(event) {
  182. return fig.toolbar_button_onmouseover(event['data']);
  183. }
  184. for(var toolbar_ind in mpl.toolbar_items) {
  185. var name = mpl.toolbar_items[toolbar_ind][0];
  186. var tooltip = mpl.toolbar_items[toolbar_ind][1];
  187. var image = mpl.toolbar_items[toolbar_ind][2];
  188. var method_name = mpl.toolbar_items[toolbar_ind][3];
  189. if (!name) {
  190. // put a spacer in here.
  191. continue;
  192. }
  193. var button = $('<button/>');
  194. button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +
  195. 'ui-button-icon-only');
  196. button.attr('role', 'button');
  197. button.attr('aria-disabled', 'false');
  198. button.click(method_name, toolbar_event);
  199. button.mouseover(tooltip, toolbar_mouse_event);
  200. var icon_img = $('<span/>');
  201. icon_img.addClass('ui-button-icon-primary ui-icon');
  202. icon_img.addClass(image);
  203. icon_img.addClass('ui-corner-all');
  204. var tooltip_span = $('<span/>');
  205. tooltip_span.addClass('ui-button-text');
  206. tooltip_span.html(tooltip);
  207. button.append(icon_img);
  208. button.append(tooltip_span);
  209. nav_element.append(button);
  210. }
  211. var fmt_picker_span = $('<span/>');
  212. var fmt_picker = $('<select/>');
  213. fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');
  214. fmt_picker_span.append(fmt_picker);
  215. nav_element.append(fmt_picker_span);
  216. this.format_dropdown = fmt_picker[0];
  217. for (var ind in mpl.extensions) {
  218. var fmt = mpl.extensions[ind];
  219. var option = $(
  220. '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);
  221. fmt_picker.append(option);
  222. }
  223. // Add hover states to the ui-buttons
  224. $( ".ui-button" ).hover(
  225. function() { $(this).addClass("ui-state-hover");},
  226. function() { $(this).removeClass("ui-state-hover");}
  227. );
  228. var status_bar = $('<span class="mpl-message"/>');
  229. nav_element.append(status_bar);
  230. this.message = status_bar[0];
  231. }
  232. mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {
  233. // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,
  234. // which will in turn request a refresh of the image.
  235. this.send_message('resize', {'width': x_pixels, 'height': y_pixels});
  236. }
  237. mpl.figure.prototype.send_message = function(type, properties) {
  238. properties['type'] = type;
  239. properties['figure_id'] = this.id;
  240. this.ws.send(JSON.stringify(properties));
  241. }
  242. mpl.figure.prototype.send_draw_message = function() {
  243. if (!this.waiting) {
  244. this.waiting = true;
  245. this.ws.send(JSON.stringify({type: "draw", figure_id: this.id}));
  246. }
  247. }
  248. mpl.figure.prototype.handle_save = function(fig, msg) {
  249. var format_dropdown = fig.format_dropdown;
  250. var format = format_dropdown.options[format_dropdown.selectedIndex].value;
  251. fig.ondownload(fig, format);
  252. }
  253. mpl.figure.prototype.handle_resize = function(fig, msg) {
  254. var size = msg['size'];
  255. if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {
  256. fig._resize_canvas(size[0], size[1]);
  257. fig.send_message("refresh", {});
  258. };
  259. }
  260. mpl.figure.prototype.handle_rubberband = function(fig, msg) {
  261. var x0 = msg['x0'] / mpl.ratio;
  262. var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;
  263. var x1 = msg['x1'] / mpl.ratio;
  264. var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;
  265. x0 = Math.floor(x0) + 0.5;
  266. y0 = Math.floor(y0) + 0.5;
  267. x1 = Math.floor(x1) + 0.5;
  268. y1 = Math.floor(y1) + 0.5;
  269. var min_x = Math.min(x0, x1);
  270. var min_y = Math.min(y0, y1);
  271. var width = Math.abs(x1 - x0);
  272. var height = Math.abs(y1 - y0);
  273. fig.rubberband_context.clearRect(
  274. 0, 0, fig.canvas.width / mpl.ratio, fig.canvas.height / mpl.ratio);
  275. fig.rubberband_context.strokeRect(min_x, min_y, width, height);
  276. }
  277. mpl.figure.prototype.handle_figure_label = function(fig, msg) {
  278. // Updates the figure title.
  279. fig.header.textContent = msg['label'];
  280. }
  281. mpl.figure.prototype.handle_cursor = function(fig, msg) {
  282. var cursor = msg['cursor'];
  283. switch(cursor)
  284. {
  285. case 0:
  286. cursor = 'pointer';
  287. break;
  288. case 1:
  289. cursor = 'default';
  290. break;
  291. case 2:
  292. cursor = 'crosshair';
  293. break;
  294. case 3:
  295. cursor = 'move';
  296. break;
  297. }
  298. fig.rubberband_canvas.style.cursor = cursor;
  299. }
  300. mpl.figure.prototype.handle_message = function(fig, msg) {
  301. fig.message.textContent = msg['message'];
  302. }
  303. mpl.figure.prototype.handle_draw = function(fig, msg) {
  304. // Request the server to send over a new figure.
  305. fig.send_draw_message();
  306. }
  307. mpl.figure.prototype.handle_image_mode = function(fig, msg) {
  308. fig.image_mode = msg['mode'];
  309. }
  310. mpl.figure.prototype.updated_canvas_event = function() {
  311. // Called whenever the canvas gets updated.
  312. this.send_message("ack", {});
  313. }
  314. // A function to construct a web socket function for onmessage handling.
  315. // Called in the figure constructor.
  316. mpl.figure.prototype._make_on_message_function = function(fig) {
  317. return function socket_on_message(evt) {
  318. if (evt.data instanceof Blob) {
  319. /* FIXME: We get "Resource interpreted as Image but
  320. * transferred with MIME type text/plain:" errors on
  321. * Chrome. But how to set the MIME type? It doesn't seem
  322. * to be part of the websocket stream */
  323. evt.data.type = "image/png";
  324. /* Free the memory for the previous frames */
  325. if (fig.imageObj.src) {
  326. (window.URL || window.webkitURL).revokeObjectURL(
  327. fig.imageObj.src);
  328. }
  329. fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(
  330. evt.data);
  331. fig.updated_canvas_event();
  332. fig.waiting = false;
  333. return;
  334. }
  335. else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == "data:image/png;base64") {
  336. fig.imageObj.src = evt.data;
  337. fig.updated_canvas_event();
  338. fig.waiting = false;
  339. return;
  340. }
  341. var msg = JSON.parse(evt.data);
  342. var msg_type = msg['type'];
  343. // Call the "handle_{type}" callback, which takes
  344. // the figure and JSON message as its only arguments.
  345. try {
  346. var callback = fig["handle_" + msg_type];
  347. } catch (e) {
  348. console.log("No handler for the '" + msg_type + "' message type: ", msg);
  349. return;
  350. }
  351. if (callback) {
  352. try {
  353. // console.log("Handling '" + msg_type + "' message: ", msg);
  354. callback(fig, msg);
  355. } catch (e) {
  356. console.log("Exception inside the 'handler_" + msg_type + "' callback:", e, e.stack, msg);
  357. }
  358. }
  359. };
  360. }
  361. // from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas
  362. mpl.findpos = function(e) {
  363. //this section is from http://www.quirksmode.org/js/events_properties.html
  364. var targ;
  365. if (!e)
  366. e = window.event;
  367. if (e.target)
  368. targ = e.target;
  369. else if (e.srcElement)
  370. targ = e.srcElement;
  371. if (targ.nodeType == 3) // defeat Safari bug
  372. targ = targ.parentNode;
  373. // jQuery normalizes the pageX and pageY
  374. // pageX,Y are the mouse positions relative to the document
  375. // offset() returns the position of the element relative to the document
  376. var x = e.pageX - $(targ).offset().left;
  377. var y = e.pageY - $(targ).offset().top;
  378. return {"x": x, "y": y};
  379. };
  380. /*
  381. * return a copy of an object with only non-object keys
  382. * we need this to avoid circular references
  383. * http://stackoverflow.com/a/24161582/3208463
  384. */
  385. function simpleKeys (original) {
  386. return Object.keys(original).reduce(function (obj, key) {
  387. if (typeof original[key] !== 'object')
  388. obj[key] = original[key]
  389. return obj;
  390. }, {});
  391. }
  392. mpl.figure.prototype.mouse_event = function(event, name) {
  393. var canvas_pos = mpl.findpos(event)
  394. if (name === 'button_press')
  395. {
  396. this.canvas.focus();
  397. this.canvas_div.focus();
  398. }
  399. var x = canvas_pos.x * mpl.ratio;
  400. var y = canvas_pos.y * mpl.ratio;
  401. this.send_message(name, {x: x, y: y, button: event.button,
  402. step: event.step,
  403. guiEvent: simpleKeys(event)});
  404. /* This prevents the web browser from automatically changing to
  405. * the text insertion cursor when the button is pressed. We want
  406. * to control all of the cursor setting manually through the
  407. * 'cursor' event from matplotlib */
  408. event.preventDefault();
  409. return false;
  410. }
  411. mpl.figure.prototype._key_event_extra = function(event, name) {
  412. // Handle any extra behaviour associated with a key event
  413. }
  414. mpl.figure.prototype.key_event = function(event, name) {
  415. // Prevent repeat events
  416. if (name == 'key_press')
  417. {
  418. if (event.which === this._key)
  419. return;
  420. else
  421. this._key = event.which;
  422. }
  423. if (name == 'key_release')
  424. this._key = null;
  425. var value = '';
  426. if (event.ctrlKey && event.which != 17)
  427. value += "ctrl+";
  428. if (event.altKey && event.which != 18)
  429. value += "alt+";
  430. if (event.shiftKey && event.which != 16)
  431. value += "shift+";
  432. value += 'k';
  433. value += event.which.toString();
  434. this._key_event_extra(event, name);
  435. this.send_message(name, {key: value,
  436. guiEvent: simpleKeys(event)});
  437. return false;
  438. }
  439. mpl.figure.prototype.toolbar_button_onclick = function(name) {
  440. if (name == 'download') {
  441. this.handle_save(this, null);
  442. } else {
  443. this.send_message("toolbar_button", {name: name});
  444. }
  445. };
  446. mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {
  447. this.message.textContent = tooltip;
  448. };