trace_interaction_handlers.js 4.65 KB
Newer Older
Ivan Bogatyy's avatar
Ivan Bogatyy committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

/**
 * @fileoverview This file contains click handlers for a DRAGNN trace graph.
 */
const _ = require('lodash');

/**
 * Handler for when a node is clicked.
 *
 * We first highlight the node's neighbors and 2nd-order neighbors differently,
 * giving them an aesthetically-pleasing fade-out.
 *
 * We then shift all of the components around, such that the larger (2nd-order)
 * selection elements are centered on the screen. This has the effect of making
 * edges between different components more legible, since they are not at as
 * much of an extreme angle.
 *
 * @param {!Object} e Cytoscape event object.
 * @param {!Object} cy Main Cytoscape graph object.
 * @param {boolean} horizontal Whether the layout is horizontal.
 */
const onSelectNode = function(e, cy, horizontal) {
  const node = e.cyTarget;
  // For selecting components, join all children.
  const withChildren = node.union(node.children());
  const selection =
      withChildren.union(withChildren.neighborhood()).filter(':visible');
  const _neighborhood = selection.neighborhood().filter(':visible');
  const nearbyOnly = _neighborhood.difference(selection);
  const nearbyAndSelected = _neighborhood.union(selection);

  // Reset faded, then set it on nodes/edges that should have it.
  cy.batch(() => {
    cy.elements().removeClass('faded-near faded-far');
    nearbyOnly.addClass('faded-near');
    nearbyAndSelected.abscomp().addClass('faded-far');
  });

  // Shift around components so they line up.
  const stepDim = horizontal ? 'x' : 'y';
  if (node.hasClass('step')) {
    const selectedStepNodes = nearbyAndSelected.nodes().filter('.step');
    const nodeGroups =
        _.groupBy(selectedStepNodes, (node) => node.data('parent'));
    const means = _.mapValues(
        nodeGroups,
        (nodes) => _.mean(_.map(nodes, node => node.position()[stepDim])));
    _.each(cy.$('node.component'), (compNode) => {
      if (means[compNode.id()] === undefined) {
        return;
      }
      const offset = _.mean(_.values(means)) - means[compNode.id()];
      _.each(compNode.children(), (node) => {
        node.position(stepDim, node.position()[stepDim] + offset);
      });
    });
  }

  // Zoom to fit the selection
  cy.fit(nearbyAndSelected, 60);
};

class HighlightHandler {
  /**
   * Constructs a new highlight handler.
   *
   * @param {!Object} cy Cytoscape graph controller.
   * @param {!Object} view Preact view component (InteractiveGraph)
   */
  constructor(cy, view) {
    this.cy = cy;
    this.view = view;
    this.mouseoverEdgeIds = [];
    this.debouncedHighlight =
        _.debounce(this.setNewEdgeHighlight.bind(this), 10);
  }

  /**
   * Handles an event.
   *
   * Typically called through debouncedHighlight().
   *
   * @param {?Object} e Cytoscape event from cy.on() handlers; can be null
   *     on mouse-out.
   * @param {?Object} node Cytoscape node for a mouse-over; null to trigger
   *     mouse-out.
   */
  handleEvent(e, node) {
    if (node == null) {
      // mouseout-type handler
      this.view.hideNodeInfo();
      this.debouncedHighlight(this.cy.collection([]));
    } else {
      this.view.showNodeInfo(node, {
        x: e.originalEvent.offsetX,
        y: e.originalEvent.offsetY,
      });
      this.debouncedHighlight(node.neighborhood().edges());
    }
  }

  /**
   * Switches the highlight from one set of edges to the next. Quickly compares
   * the IDs of the new set of edges and the old, so we only update the
   * highlight if it changes.
   *
   * @param {!Object} edges Cytoscape collection of new edges.
   */
  setNewEdgeHighlight(edges) {
    const newEdgeIds = _.map(edges, (e) => e.id());
    newEdgeIds.sort();
    if (!_.isEqual(newEdgeIds, this.mouseoverEdgeIds)) {
      this.cy.edges().removeClass('highlighted-edge');
      edges.addClass('highlighted-edge');
      this.mouseoverEdgeIds = newEdgeIds;
    }
  }
}

/**
 * Adds click/hover handlers to a Cytoscape graph.
 *
 * @param {!Object} cy Main Cytoscape graph object
 * @param {!InteractiveGraph} view Preact view component
 */
export default function setupTraceInteractionHandlers(cy, view) {
  const handler = new HighlightHandler(cy, view);
  const debouncedHandler = handler.handleEvent.bind(handler);

  cy.on('mouseover mousemove', 'node.step', (e) => {
    debouncedHandler(e, e.cyTarget);
  });

  cy.on('mouseout', 'node', () => {
    debouncedHandler(null, null);
  });

  cy.on('tap', 'node', e => {
    // Since onSelectNode is going to move around components, it makes sense
    // to clear the highlight.
    debouncedHandler(null, null);
    onSelectNode(e, cy, view.state.horizontal);
  });

  cy.on('tap', function(e) {
    if (e.cyTarget === cy) {
      cy.elements().removeClass('faded-near faded-far');
    }
  });
};