visualize.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /**
  2. * @fileoverview This file is the main entrypoint for DRAGNN trace
  3. * visualization. The main function that it exposes is visualizeToDiv(), which
  4. * it will save to `window` (supposing `window` is defined).
  5. */
  6. const cytoscape = require('cytoscape');
  7. const _ = require('lodash');
  8. // Legacy JS, downloaded in the Dockerfile from github. This generates
  9. // colorblind-friendly color palettes, for a variable number of components.
  10. //
  11. // This library consumes 27kb minified-but-uncompressed. Currently we mostly
  12. // serve JS to high-bandwidth clients like IPython, but if minified size is ever
  13. // a concern, it should be fairly easy to pre-compute our color palettes.
  14. const palette = require('exports-loader?palette!../palette.js');
  15. // Register our custom DRAGNN layout class.
  16. const DragnnLayout = require('./dragnn_layout.js');
  17. cytoscape('layout', 'dragnn', DragnnLayout);
  18. import preact from 'preact';
  19. import InteractiveGraph from './interactive_graph.jsx';
  20. import setupTraceInteractionHandlers from './trace_interaction_handlers';
  21. // Helper class to build a Cytoscape graph from a DRAGNN master spec.
  22. class DragnnCytoscapeGraphBuilder {
  23. /**
  24. * Creates a new DragnnCytoscapeGraphBuilder.
  25. */
  26. constructor() {
  27. this.graph = {nodes: [], edges: []};
  28. this.nodesWithoutCaptions = 0;
  29. }
  30. /**
  31. * Adds a new component node to the graph. Component nodes represent
  32. * components in DRAGNN, which often resemble tasks (tagging, parsing, etc.).
  33. *
  34. * @param {!Object} component Component descriptor from the master spec.
  35. */
  36. addComponentNode(component) {
  37. this.graph.nodes.push({
  38. 'data': {
  39. 'id': component.name,
  40. 'idx': component.idx,
  41. 'componentColor': '#' + component.color,
  42. 'text': component.name,
  43. 'type': 'component',
  44. },
  45. 'classes': 'component',
  46. });
  47. }
  48. /**
  49. * Adds a new step node to the graph. Step nodes are generated by unrolling a
  50. * component on a concrete example.
  51. *
  52. * @param {!Object} component Component descriptor from the master spec.
  53. * @param {!Object} step Step descriptor from the master spec.
  54. * @return {string} ID fo the node created.
  55. */
  56. addNode(component, step) {
  57. const graphId = component.name + '-' + step.idx;
  58. this.graph.nodes.push({
  59. 'data': {
  60. 'id': graphId,
  61. 'componentIdx': component.idx,
  62. 'stepIdx': step.idx,
  63. 'parent': component.name,
  64. 'text': step.caption || graphId,
  65. 'componentColor': '#' + component.color,
  66. 'type': 'step',
  67. // Shown in the mouse-over (node_info.jsx).
  68. 'stateInfo': step.html_representation,
  69. 'fixedFeatures': step.fixed_feature_trace,
  70. },
  71. 'classes': 'step',
  72. });
  73. return graphId;
  74. }
  75. /**
  76. * Adds a list of components from the master spec.
  77. *
  78. * This function generates colors, and calls addComponent().
  79. *
  80. * @param {!Object} masterTrace Master trace proto from DRAGNN.
  81. */
  82. addComponents(masterTrace) {
  83. const colors = palette('tol', masterTrace.component_trace.length);
  84. if (colors == null) {
  85. // Apparently palette.js can fail with > 12 components or such.
  86. window.alert('FAILURE -- YOU HAVE TOO MANY COMPONENTS FOR palette.js.');
  87. return;
  88. }
  89. _.each(masterTrace.component_trace, function(component, idx) {
  90. component.idx = idx;
  91. component.color = colors[idx];
  92. this.addComponent(component);
  93. }.bind(this));
  94. console.log('' + this.nodesWithoutCaptions + ' nodes without captions');
  95. }
  96. /**
  97. * Adds one component from the master spec. This generates component nodes,
  98. * step nodes, and edges.
  99. *
  100. * @param {!Object} component Component descriptor from the master spec.
  101. */
  102. addComponent(component) {
  103. this.addComponentNode(component);
  104. _.each(component.step_trace, (step, idx) => {
  105. step.idx = idx;
  106. if (!step.caption) {
  107. this.nodesWithoutCaptions += 1;
  108. return;
  109. }
  110. const graphId = this.addNode(component, step);
  111. _.each(step.linked_feature_trace, (linkedFeature) => {
  112. // Each feature can take multiple values.
  113. _.each(linkedFeature.value_trace, (linkedValue) => {
  114. const srcGraphId =
  115. linkedFeature.source_component + '-' + linkedValue.step_idx;
  116. this.graph.edges.push({
  117. 'data': {
  118. 'source': srcGraphId,
  119. 'target': graphId,
  120. 'curvature': 0,
  121. 'featureName': linkedValue.feature_name,
  122. 'featureValue': linkedValue.feature_value,
  123. }
  124. });
  125. });
  126. });
  127. });
  128. }
  129. }
  130. /**
  131. * Component for a graph and its controls. Currently this just manually
  132. * calls DOM methods, but we'll switch it out for something more modern in a
  133. * sec.
  134. */
  135. class InteractiveDragnnGraph {
  136. /**
  137. * Controller for the entire DRAGNN graph element on a page.
  138. *
  139. * @param {!Object} masterTrace Master trace proto from DRAGNN.
  140. * @param {!Object} element Container DOM element to populate.
  141. */
  142. constructor(masterTrace, element) {
  143. this.masterTrace = masterTrace;
  144. this.element = element;
  145. }
  146. /**
  147. * Initializes the controls and the graph.
  148. */
  149. initDomElements() {
  150. this.element.style.position = 'relative';
  151. this.element.style.overflow = 'hidden';
  152. const elt = preact.h(InteractiveGraph, {
  153. onmount: (view, graph_elt) => {
  154. this.view = view;
  155. this.initializeGraph(view, graph_elt);
  156. },
  157. onfilter: this.onFilter.bind(this),
  158. });
  159. preact.render(elt, this.element);
  160. }
  161. /**
  162. * Handler for when filtering text is entered.
  163. *
  164. * Future features: Make the number of neighbors customizable.
  165. *
  166. * @param {string} text Regular expression text to filter with. Currently
  167. * applied to node labels.
  168. */
  169. onFilter(text) {
  170. // Show relevant nodes (and parent components).
  171. const re = new RegExp(text);
  172. let sel = this.cy.$('node.step').filter(function(i, node) {
  173. return !text || node.data('text').match(re);
  174. });
  175. sel = sel.union(sel.neighborhood());
  176. sel = sel.union(sel.parents());
  177. sel.nodes().show();
  178. sel.abscomp().nodes().hide();
  179. // Redo layout.
  180. this.cy.layout({name: 'dragnn'});
  181. }
  182. /**
  183. * Initializes the Cytoscape graph.
  184. */
  185. initializeGraph(view, domElement) {
  186. const builder = new DragnnCytoscapeGraphBuilder();
  187. builder.addComponents(this.masterTrace);
  188. const cy = cytoscape({
  189. container: domElement,
  190. boxSelectionEnabled: true,
  191. autounselectify: true,
  192. // We'll do more custom layout later.
  193. layout: {name: 'dragnn'},
  194. style: [
  195. {
  196. selector: 'node',
  197. style: {
  198. 'background-color': 'data(componentColor)',
  199. 'content': 'data(text)',
  200. 'text-halign': 'center',
  201. 'text-opacity': 1.0,
  202. 'text-valign': 'center',
  203. }
  204. },
  205. {
  206. selector: 'node.step',
  207. style: {
  208. 'text-outline-width': 2,
  209. 'text-outline-color': '#ffffff',
  210. 'text-outline-opacity': 0.3,
  211. }
  212. },
  213. {
  214. selector: ':parent',
  215. style: {
  216. 'background-opacity': 0.1,
  217. 'text-halign': 'right',
  218. 'text-margin-x': 5,
  219. 'text-margin-y': 5,
  220. 'text-valign': 'bottom',
  221. }
  222. },
  223. {
  224. selector: 'edge',
  225. style: {
  226. 'control-point-distance': 'data(curvature)',
  227. 'curve-style': 'unbundled-bezier',
  228. 'line-color': '#666666',
  229. 'opacity': 0.4,
  230. 'target-arrow-color': '#666666',
  231. 'target-arrow-shape': 'triangle',
  232. 'width': 3,
  233. }
  234. },
  235. {selector: 'edge.faded-near', style: {'opacity': 0.2}},
  236. {selector: 'node.step.faded-near', style: {'opacity': 0.5}},
  237. {
  238. selector: 'node.step.faded-far, edge.faded-far',
  239. style: {'opacity': 0.1}
  240. },
  241. // Overall, override stuff for mouse-overs, but don't make the far edges
  242. // too dark (that looks jarring).
  243. {
  244. selector: 'edge.highlighted-edge',
  245. style: {'line-color': '#333333', 'opacity': 1.0}
  246. },
  247. {
  248. selector: 'edge.highlighted-edge.faded-far',
  249. style: {
  250. 'opacity': 0.4,
  251. }
  252. },
  253. ],
  254. elements: builder.graph,
  255. });
  256. this.cy = cy;
  257. setupTraceInteractionHandlers(cy, view, this.element);
  258. }
  259. }
  260. /**
  261. * This is the external interface. See "index.html" for the development example,
  262. * which downloads graph data from a JSON file. In most iPython notebook
  263. * situations, the script tag containing the graph definition will be generated
  264. * inline.
  265. *
  266. * @param {Object} masterTrace Master trace proto from DRAGNN.
  267. * @param {string} divId ID of the page element to populate with the graph.
  268. */
  269. const visualizeToDiv = function(masterTrace, divId) {
  270. const interactiveGraph =
  271. new InteractiveDragnnGraph(masterTrace, document.getElementById(divId));
  272. interactiveGraph.initDomElements();
  273. };
  274. if (window !== undefined) {
  275. window.visualizeToDiv = visualizeToDiv;
  276. }