aboutsummaryrefslogtreecommitdiffhomepage
path: root/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts
blob: 2e2467f0392e6b6345d6ddf0544446e808c3fdf7 (plain)
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
/// <reference path="../graph.ts" />
/// <reference path="edge.ts" />
/// <reference path="node.ts" />
/// <reference path="../layout.ts" />

module tf.graph.scene {

/** Enums element class of objects in the scene */
export let Class = {
  Node: {
    // <g> element that contains nodes.
    CONTAINER: "nodes",
    // <g> element that contains detail about a node.
    GROUP: "node",
    // <g> element that contains visual elements (like rect, ellipse).
    SHAPE: "nodeshape",
    // <*> element(s) under SHAPE that should receive color updates.
    COLOR_TARGET: "nodecolortarget",
    // <text> element showing the node's label.
    LABEL: "nodelabel",
    // <g> element that contains all visuals for the expand/collapse
    // button for expandable group nodes.
    BUTTON_CONTAINER: "buttoncontainer",
    // <circle> element that surrounds expand/collapse buttons.
    BUTTON_CIRCLE: "buttoncircle",
    // <path> element of the expand button.
    EXPAND_BUTTON: "expandbutton",
    // <path> element of the collapse button.
    COLLAPSE_BUTTON: "collapsebutton"
  },
  Edge: {
    CONTAINER: "edges",
    GROUP: "edge",
    LINE: "edgeline",
    REF_LINE: "refline",
    STRUCTURAL: "structural"
  },
  Annotation: {
    OUTBOX: "out-annotations",
    INBOX: "in-annotations",
    GROUP: "annotation",
    NODE: "annotation-node",
    EDGE: "annotation-edge",
    CONTROL_EDGE: "annotation-control-edge",
    LABEL: "annotation-label",
    ELLIPSIS: "annotation-ellipsis"
  },
  Scene: {
    GROUP: "scene",
    CORE: "core",
    INEXTRACT: "in-extract",
    OUTEXTRACT: "out-extract"
  },
  Subscene: {
    GROUP: "subscene"
  },
  OPNODE: "op",
  METANODE: "meta",
  SERIESNODE: "series",
  BRIDGENODE: "bridge",
  ELLIPSISNODE: "ellipsis"
};

/**
 * Helper method for fitting the graph in the svg view.
 *
 * @param svg The main svg.
 * @param zoomG The svg group used for panning and zooming.
 * @param d3zoom The zoom behavior.
 * @param callback Called when the fitting is done.
 */
export function fit(svg, zoomG, d3zoom, callback) {
  let svgRect = svg.getBoundingClientRect();
  let sceneSize = zoomG.getBBox();
  let scale = 0.9 * Math.min(
      svgRect.width / sceneSize.width,
      svgRect.height / sceneSize.height,
      2
    );
  let params = layout.PARAMS.graph;
  let zoomEvent = d3zoom.scale(scale)
    .on("zoomend.fitted", () => {
      // Remove the listener for the zoomend event,
      // so we don't get called at the end of regular zoom events,
      // just those that fit the graph to screen.
      d3zoom.on("zoomend.fitted", null);
      callback();
    })
    .translate([params.padding.paddingLeft, params.padding.paddingTop])
    .event;
  d3.select(zoomG).transition().duration(500).call(zoomEvent);
};

/**
 * Helper method for panning the graph to center on the provided node,
 * if the node is currently off-screen.
 *
 * @param nodeName The node to center the graph on
 * @param svg The root SVG element for the graph
 * @param zoomG The svg group used for panning and zooming.
 * @param d3zoom The zoom behavior.
 * @return True if the graph had to be panned to display the
 *            provided node.
 */
export function panToNode(nodeName: String, svg, zoomG, d3zoom): boolean {
  let node: any = d3.selectAll("[data-name='" + nodeName + "']."
    + Class.Node.GROUP)[0][0];
  if (!node) {
    return false;
  }
  let translate = d3zoom.translate();
  // Check if the selected node is off-screen in either
  // X or Y dimension in either direction.
  let nodeBox = node.getBBox();
  let nodeCtm = node.getScreenCTM();
  let pointTL = svg.createSVGPoint();
  let pointBR = svg.createSVGPoint();
  pointTL.x = nodeBox.x;
  pointTL.y = nodeBox.y;
  pointBR.x = nodeBox.x + nodeBox.width;
  pointBR.y = nodeBox.y + nodeBox.height;
  pointTL = pointTL.matrixTransform(nodeCtm);
  pointBR = pointBR.matrixTransform(nodeCtm);
  let isOutsideOfBounds = (start, end, bound) => {
    return end < 0 || start > bound;
  };
  let svgRect = svg.getBoundingClientRect();
  if (isOutsideOfBounds(pointTL.x, pointBR.x, svgRect.width) ||
      isOutsideOfBounds(pointTL.y, pointBR.y, svgRect.height)) {
    // Determine the amount to transform the graph in both X and Y
    // dimensions in order to center the selected node. This takes into
    // acount the position of the node, the size of the svg scene, the
    // amount the scene has been scaled by through zooming, and any previous
    // transform already performed by this logic.
    let centerX = (pointTL.x + pointBR.x) / 2;
    let centerY = (pointTL.y + pointBR.y) / 2;
    let dx = ((svgRect.width / 2) - centerX);
    let dy = ((svgRect.height / 2) - centerY);
    let zoomEvent = d3zoom.translate([translate[0] + dx, translate[1] + dy])
        .event;
    d3.select(zoomG).transition().duration(500).call(zoomEvent);
    return true;
  }
  return false;
};

/**
 * Given a container d3 selection, select a child svg element of a given tag
 * and class if exists or append / insert one otherwise.  If multiple children
 * matches the tag and class name, returns only the first one.
 *
 * @param container
 * @param tagName tag name.
 * @param className (optional) Class name.
 * @param before (optional) reference DOM node for insertion.
 * @return selection of the element
 */
export function selectOrCreateChild(container, tagName: string,
    className?: string, before?) {
  let child = selectChild(container, tagName, className);
  if (!child.empty()) {
    return child;
  }
  let newElement = document.createElementNS("http://www.w3.org/2000/svg",
    tagName);
  if (className) {
    newElement.classList.add(className);
  }

  if (before) { // if before exists, insert
    container.node().insertBefore(newElement, before);
  } else { // otherwise, append
    container.node().appendChild(newElement);
  }
  return d3.select(newElement)
           // need to bind data to emulate d3_selection.append
           .datum(container.datum());
};

/**
 * Given a container d3 selection, select a child element of a given tag and
 * class. If multiple children matches the tag and class name, returns only
 * the first one.
 *
 * @param container
 * @param tagName tag name.
 * @param className (optional) Class name.
 * @return selection of the element, or an empty selection
 */
export function selectChild(container, tagName: string, className?: string) {
  let children = container.node().childNodes;
  for (let i = 0; i < children.length; i++) {
    let child = children[i];
    if (child.tagName === tagName &&
        (!className || child.classList.contains(className))
          ) {
      return d3.select(child);
    }
  }
  return d3.select(null);
};

/**
 * Select or create a sceneGroup and build/update its nodes and edges.
 *
 * Structure Pattern:
 *
 * <g class="scene">
 *   <g class="core">
 *     <g class="edges">
 *       ... stuff from tf.graph.scene.edges.build ...
 *     </g>
 *     <g class="nodes">
 *       ... stuff from tf.graph.scene.nodes.build ...
 *     </g>
 *   </g>
 *   <g class="in-extract">
 *     <g class="nodes">
 *       ... stuff from tf.graph.scene.nodes.build ...
 *     </g>
 *   </g>
 *   <g class="out-extract">
 *     <g class="nodes">
 *       ... stuff from tf.graph.scene.nodes.build ...
 *     </g>
 *   </g>
 * </g>
 *
 * @param container D3 selection of the parent.
 * @param renderNode render node of a metanode or series node.
 * @param sceneBehavior Parent scene module.
 * @param sceneClass class attribute of the scene (default="scene").
 */
export function buildGroup(container,
    renderNode: render.RenderGroupNodeInformation,
    sceneBehavior,
    sceneClass: string) {
  sceneClass = sceneClass || Class.Scene.GROUP;
  let isNewSceneGroup = selectChild(container, "g", sceneClass).empty();
  let sceneGroup = selectOrCreateChild(container, "g", sceneClass);

  // core
  let coreGroup = selectOrCreateChild(sceneGroup, "g", Class.Scene.CORE);
  let coreNodes = _.reduce(renderNode.coreGraph.nodes(), (nodes, name) => {
                    let node = renderNode.coreGraph.node(name);
                    if (!node.excluded) {
                      nodes.push(node);
                    }
                    return nodes;
                  }, []);

  if (renderNode.node.type === NodeType.SERIES) {
    // For series, we want the first item on top, so reverse the array so
    // the first item in the series becomes last item in the top, and thus
    // is rendered on the top.
    coreNodes.reverse();
  }

  // Create the layer of edges for this scene (paths).
  edge.buildGroup(coreGroup, renderNode.coreGraph, sceneBehavior);

  // Create the layer of nodes for this scene (ellipses, rects etc).
  node.buildGroup(coreGroup, coreNodes, sceneBehavior);

  // In-extract
  if (renderNode.isolatedInExtract.length > 0) {
    let inExtractGroup = selectOrCreateChild(sceneGroup, "g",
      Class.Scene.INEXTRACT);
    node.buildGroup(inExtractGroup, renderNode.isolatedInExtract,
        sceneBehavior);
  } else {
    selectChild(sceneGroup, "g", Class.Scene.INEXTRACT).remove();
  }

  // Out-extract
  if (renderNode.isolatedOutExtract.length > 0) {
    let outExtractGroup = selectOrCreateChild(sceneGroup, "g",
      Class.Scene.OUTEXTRACT);
    node.buildGroup(outExtractGroup, renderNode.isolatedOutExtract,
        sceneBehavior);
  } else {
    selectChild(sceneGroup, "g", Class.Scene.OUTEXTRACT).remove();
  }

  position(sceneGroup, renderNode);

  // Fade in the scene group if it didn't already exist.
  if (isNewSceneGroup) {
    sceneGroup.attr("opacity", 0)
      .transition().attr("opacity", 1);
  }

  return sceneGroup;
};

/**
 * Given a scene's svg group, set  g.in-extract, g.coreGraph, g.out-extract svg
 * groups' position relative to the scene.
 *
 * @param sceneGroup
 * @param renderNode render node of a metanode or series node.
 */
function position(sceneGroup, renderNode: render.RenderGroupNodeInformation) {
  // Translate scenes down by the label height so that when showing graphs in
  // expanded metanodes, the graphs are below the labels.  Do not shift them
  // down for series nodes as series nodes don't have labels inside of their
  // bounding boxes.
  let yTranslate = renderNode.node.type === NodeType.SERIES ?
    0 : layout.PARAMS.subscene.meta.labelHeight;

  // core
  translate(selectChild(sceneGroup, "g", Class.Scene.CORE),
                  0, yTranslate);

  // in-extract
  let inExtractX = renderNode.coreBox.width === 0 ?
    0 : renderNode.coreBox.width;
  let hasInExtract = renderNode.isolatedInExtract.length > 0;
  if (hasInExtract) {
    translate(selectChild(sceneGroup, "g", Class.Scene.INEXTRACT),
                    inExtractX, yTranslate);
  }

  // out-extract
  let hasOutExtract = renderNode.isolatedOutExtract.length > 0;
  if (hasOutExtract) {
    let outExtractX = inExtractX + renderNode.inExtractBox.width
      + renderNode.extractXOffset;
    translate(selectChild(sceneGroup, "g", Class.Scene.OUTEXTRACT),
                    outExtractX, yTranslate);
  }
};

/** Adds a click listener to a group that fires a graph-select event */
export function addGraphClickListener(graphGroup, sceneBehavior) {
  d3.select(graphGroup).on("click", () => {
    sceneBehavior.fire("graph-select");
  });
};

/** Helper for adding transform: translate(x0, y0) */
export function translate(selection, x0: number, y0: number) {
  selection.attr("transform", "translate(" + x0 + "," + y0 + ")");
};

/**
 * Helper for setting position of a svg rect
 * @param rect rect to set position of.
 * @param cx Center x.
 * @param cy Center x.
 * @param width Width to set.
 * @param height Height to set.
 */
export function positionRect(rect, cx: number, cy: number, width: number,
    height: number) {
  rect.transition().attr({
    x: cx - width / 2,
    y: cy - height / 2,
    width: width,
    height: height
  });
};

/**
 * Helper for setting position of a svg expand/collapse button
 * @param button container group
 * @param renderNode the render node of the group node to position
 *        the button on.
 */
export function positionButton(button,
    renderNode: render.RenderNodeInformation) {
  // Position the button in the top-right corner of the group node,
  // with space given the draw the button inside of the corner.
  let x = renderNode.x + renderNode.width / 2 - 6;
  let y = renderNode.y - renderNode.height / 2 + 6;
  // For unexpanded series nodes, the button has special placement due
  // to the unique visuals of this group node.
  if (renderNode.node.type === NodeType.SERIES && !renderNode.expanded) {
    x += 10;
    y -= 2;
  }
  let translateStr = "translate(" + x + "," + y + ")";
  button.selectAll("path").transition().attr("transform", translateStr);
  button.select("circle").transition().attr({
    cx: x,
    cy: y,
    r: layout.PARAMS.nodeSize.meta.expandButtonRadius
  });
};

/**
 * Helper for setting position of a svg ellipse
 * @param ellipse ellipse to set position of.
 * @param cx Center x.
 * @param cy Center x.
 * @param width Width to set.
 * @param height Height to set.
 */
export function positionEllipse(ellipse, cx: number, cy: number,
    width: number, height: number) {
  ellipse.transition().attr({
    cx: cx,
    cy: cy,
    rx: width / 2,
    ry: height / 2
  });
};

} // close module