trace_interaction_handlers.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. /**
  2. * @fileoverview This file contains click handlers for a DRAGNN trace graph.
  3. */
  4. const _ = require('lodash');
  5. /**
  6. * Handler for when a node is clicked.
  7. *
  8. * We first highlight the node's neighbors and 2nd-order neighbors differently,
  9. * giving them an aesthetically-pleasing fade-out.
  10. *
  11. * We then shift all of the components around, such that the larger (2nd-order)
  12. * selection elements are centered on the screen. This has the effect of making
  13. * edges between different components more legible, since they are not at as
  14. * much of an extreme angle.
  15. *
  16. * @param {!Object} e Cytoscape event object.
  17. * @param {!Object} cy Main Cytoscape graph object.
  18. * @param {boolean} horizontal Whether the layout is horizontal.
  19. */
  20. const onSelectNode = function(e, cy, horizontal) {
  21. const node = e.cyTarget;
  22. // For selecting components, join all children.
  23. const withChildren = node.union(node.children());
  24. const selection =
  25. withChildren.union(withChildren.neighborhood()).filter(':visible');
  26. const _neighborhood = selection.neighborhood().filter(':visible');
  27. const nearbyOnly = _neighborhood.difference(selection);
  28. const nearbyAndSelected = _neighborhood.union(selection);
  29. // Reset faded, then set it on nodes/edges that should have it.
  30. cy.batch(() => {
  31. cy.elements().removeClass('faded-near faded-far');
  32. nearbyOnly.addClass('faded-near');
  33. nearbyAndSelected.abscomp().addClass('faded-far');
  34. });
  35. // Shift around components so they line up.
  36. const stepDim = horizontal ? 'x' : 'y';
  37. if (node.hasClass('step')) {
  38. const selectedStepNodes = nearbyAndSelected.nodes().filter('.step');
  39. const nodeGroups =
  40. _.groupBy(selectedStepNodes, (node) => node.data('parent'));
  41. const means = _.mapValues(
  42. nodeGroups,
  43. (nodes) => _.mean(_.map(nodes, node => node.position()[stepDim])));
  44. _.each(cy.$('node.component'), (compNode) => {
  45. if (means[compNode.id()] === undefined) {
  46. return;
  47. }
  48. const offset = _.mean(_.values(means)) - means[compNode.id()];
  49. _.each(compNode.children(), (node) => {
  50. node.position(stepDim, node.position()[stepDim] + offset);
  51. });
  52. });
  53. }
  54. // Zoom to fit the selection
  55. cy.fit(nearbyAndSelected, 60);
  56. };
  57. class HighlightHandler {
  58. /**
  59. * Constructs a new highlight handler.
  60. *
  61. * @param {!Object} cy Cytoscape graph controller.
  62. * @param {!Object} view Preact view component (InteractiveGraph)
  63. */
  64. constructor(cy, view) {
  65. this.cy = cy;
  66. this.view = view;
  67. this.mouseoverEdgeIds = [];
  68. this.debouncedHighlight =
  69. _.debounce(this.setNewEdgeHighlight.bind(this), 10);
  70. }
  71. /**
  72. * Handles an event.
  73. *
  74. * Typically called through debouncedHighlight().
  75. *
  76. * @param {?Object} e Cytoscape event from cy.on() handlers; can be null
  77. * on mouse-out.
  78. * @param {?Object} node Cytoscape node for a mouse-over; null to trigger
  79. * mouse-out.
  80. */
  81. handleEvent(e, node) {
  82. if (node == null) {
  83. // mouseout-type handler
  84. this.view.hideNodeInfo();
  85. this.debouncedHighlight(this.cy.collection([]));
  86. } else {
  87. this.view.showNodeInfo(node, {
  88. x: e.originalEvent.offsetX,
  89. y: e.originalEvent.offsetY,
  90. });
  91. this.debouncedHighlight(node.neighborhood().edges());
  92. }
  93. }
  94. /**
  95. * Switches the highlight from one set of edges to the next. Quickly compares
  96. * the IDs of the new set of edges and the old, so we only update the
  97. * highlight if it changes.
  98. *
  99. * @param {!Object} edges Cytoscape collection of new edges.
  100. */
  101. setNewEdgeHighlight(edges) {
  102. const newEdgeIds = _.map(edges, (e) => e.id());
  103. newEdgeIds.sort();
  104. if (!_.isEqual(newEdgeIds, this.mouseoverEdgeIds)) {
  105. this.cy.edges().removeClass('highlighted-edge');
  106. edges.addClass('highlighted-edge');
  107. this.mouseoverEdgeIds = newEdgeIds;
  108. }
  109. }
  110. }
  111. /**
  112. * Adds click/hover handlers to a Cytoscape graph.
  113. *
  114. * @param {!Object} cy Main Cytoscape graph object
  115. * @param {!InteractiveGraph} view Preact view component
  116. */
  117. export default function setupTraceInteractionHandlers(cy, view) {
  118. const handler = new HighlightHandler(cy, view);
  119. const debouncedHandler = handler.handleEvent.bind(handler);
  120. cy.on('mouseover mousemove', 'node.step', (e) => {
  121. debouncedHandler(e, e.cyTarget);
  122. });
  123. cy.on('mouseout', 'node', () => {
  124. debouncedHandler(null, null);
  125. });
  126. cy.on('tap', 'node', e => {
  127. // Since onSelectNode is going to move around components, it makes sense
  128. // to clear the highlight.
  129. debouncedHandler(null, null);
  130. onSelectNode(e, cy, view.state.horizontal);
  131. });
  132. cy.on('tap', function(e) {
  133. if (e.cyTarget === cy) {
  134. cy.elements().removeClass('faded-near faded-far');
  135. }
  136. });
  137. };