You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by fe...@apache.org on 2017/12/23 18:36:10 UTC

zeppelin git commit: [ZEPPELIN-3038] Network visualization not show "source" and "target" node/edge properties

Repository: zeppelin
Updated Branches:
  refs/heads/master 53e6f743d -> 851dcb1a3


[ZEPPELIN-3038] Network visualization not show "source" and "target" node/edge properties

### What is this PR for?
The Network visualization not show "source" and "target" node/edge properties when the graph is flattened to create the table representation.

### What type of PR is it?
[Bug Fix]

### Todos
* [x] - Fixed

### Screenshot
Before:
![zeppelin before](https://user-images.githubusercontent.com/1833335/32510305-6ee266c8-c3f0-11e7-9e28-7ed1f304ebcb.PNG)

After:
![zeppelin after](https://user-images.githubusercontent.com/1833335/32510324-8121cc3e-c3f0-11e7-9f1f-84bba563aebc.PNG)

### What is the Jira issue?
[ZEPPELIN-3038](https://issues.apache.org/jira/browse/ZEPPELIN-3038)

### How should this be tested?
Please use this [notebook](https://gist.github.com/conker84/9574127c2389d08164423894aa93b67f)

### Questions:
* Does the licenses files need update? no
* Is there breaking changes for older versions? no
* Does this needs documentation? no

Author: Andrea Santurbano <sa...@gmail.com>

Closes #2653 from conker84/network-fix and squashes the following commits:

d4f19b6 [Andrea Santurbano] removed unused property
44100c6 [Andrea Santurbano] Removed semicolons
273c88f [Andrea Santurbano] added test on edges prevent double rendering
5eeabc2 [Andrea Santurbano] networkdata class now shows fields "source" and "target" + added test case moved the logic related to the visualization from networkdata to visualization-d3network added some optimization


Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/851dcb1a
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/851dcb1a
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/851dcb1a

Branch: refs/heads/master
Commit: 851dcb1a392a3b720501982c6c2c4a3d30468a7e
Parents: 53e6f74
Author: Andrea Santurbano <sa...@gmail.com>
Authored: Tue Nov 7 18:39:58 2017 +0100
Committer: Felix Cheung <fe...@apache.org>
Committed: Sat Dec 23 10:34:39 2017 -0800

----------------------------------------------------------------------
 zeppelin-web/src/app/tabledata/networkdata.js   | 104 +++-----------
 .../src/app/tabledata/networkdata.test.js       |  27 +++-
 .../builtins/visualization-d3network.js         | 135 ++++++++++++++-----
 3 files changed, 150 insertions(+), 116 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/851dcb1a/zeppelin-web/src/app/tabledata/networkdata.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/tabledata/networkdata.js b/zeppelin-web/src/app/tabledata/networkdata.js
index 7983d82..70cd86b 100644
--- a/zeppelin-web/src/app/tabledata/networkdata.js
+++ b/zeppelin-web/src/app/tabledata/networkdata.js
@@ -40,42 +40,40 @@ export default class NetworkData extends TableData {
       return
     }
 
-    this.setNodesDefaults()
-    this.setEdgesDefaults()
-
+    this.graph.edges = this.graph.edges || []
     this.networkNodes = angular.equals({}, this.graph.labels || {})
             ? null : {count: this.graph.nodes.length, labels: this.graph.labels}
     this.networkRelationships = angular.equals([], this.graph.types || [])
             ? null : {count: this.graph.edges.length, types: this.graph.types}
 
-    let rows = []
-    let comment = ''
-    let entities = this.graph.nodes.concat(this.graph.edges)
-    let baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'},
-                       {name: 'label', index: 1, aggr: 'sum'}]
-    let internalFieldsToJump = ['count', 'size', 'totalCount',
-      'data', 'x', 'y', 'labels']
-    let baseCols = _.map(baseColumnNames, function(col) { return col.name })
-    let keys = _.map(entities, function(elem) { return Object.keys(elem.data || {}) })
+    const rows = []
+    const comment = ''
+    const entities = this.graph.nodes.concat(this.graph.edges)
+    const baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'}]
+    const containsLabelField = _.find(entities, (entity) => 'label' in entity) != null
+    if (this.graph.labels || this.graph.types || containsLabelField) {
+      baseColumnNames.push({name: 'label', index: 1, aggr: 'sum'})
+    }
+    const internalFieldsToJump = ['count', 'size', 'totalCount',
+      'data', 'x', 'y', 'labels', 'source', 'target']
+    const baseCols = _.map(baseColumnNames, (col) => col.name)
+    let keys = _.map(entities, (elem) => Object.keys(elem.data || {}))
     keys = _.flatten(keys)
-    keys = _.uniq(keys).filter(function(key) {
-      return baseCols.indexOf(key) === -1
-    })
-    let columnNames = baseColumnNames.concat(_.map(keys, function(elem, i) {
+    keys = _.uniq(keys).filter((key) => baseCols.indexOf(key) === -1)
+    const entityColumnNames = _.map(keys, (elem, i) => {
       return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'}
-    }))
+    })
+    const columnNames = baseColumnNames.concat(entityColumnNames)
     for (let i = 0; i < entities.length; i++) {
-      let entity = entities[i]
-      let col = []
-      let col2 = []
+      const entity = entities[i]
+      const col = []
       entity.data = entity.data || {}
       for (let j = 0; j < columnNames.length; j++) {
-        let name = columnNames[j].name
-        let value = name in entity && internalFieldsToJump.indexOf(name) === -1
+        const name = columnNames[j].name
+        const value = name in entity && internalFieldsToJump.indexOf(name) === -1
             ? entity[name] : entity.data[name]
-        let parsedValue = value === null || value === undefined ? '' : value
+        const parsedValue = value === null || value === undefined ? '' : value
         col.push(parsedValue)
-        col2.push({key: name, value: parsedValue})
       }
       rows.push(col)
     }
@@ -84,62 +82,4 @@ export default class NetworkData extends TableData {
     this.columns = columnNames
     this.rows = rows
   }
-
-  setNodesDefaults() {
-  }
-
-  setEdgesDefaults() {
-    this.graph.edges
-      .sort((a, b) => {
-        if (a.source > b.source) {
-          return 1
-        } else if (a.source < b.source) {
-          return -1
-        } else if (a.target > b.target) {
-          return 1
-        } else if (a.target < b.target) {
-          return -1
-        } else {
-          return 0
-        }
-      })
-    this.graph.edges
-      .forEach((edge, index) => {
-        let prevEdge = this.graph.edges[index - 1]
-        edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target
-            ? prevEdge.count : 0) + 1
-        edge.totalCount = this.graph.edges
-          .filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target)
-          .length
-      })
-    this.graph.edges
-      .forEach((edge) => {
-        if (typeof +edge.source === 'number') {
-          edge.source = this.graph.nodes.filter((node) => +edge.source === +node.id)[0] || null
-        }
-        if (typeof +edge.target === 'number') {
-          edge.target = this.graph.nodes.filter((node) => +edge.target === +node.id)[0] || null
-        }
-      })
-  }
-
-  getNetworkProperties() {
-    let baseCols = ['id', 'label']
-    let properties = {}
-    this.graph.nodes.forEach(function(node) {
-      let hasLabel = 'label' in node && node.label !== ''
-      if (!hasLabel) {
-        return
-      }
-      let label = node.label
-      let hasKey = hasLabel && label in properties
-      let keys = _.uniq(Object.keys(node.data || {})
-              .concat(hasKey ? properties[label].keys : baseCols))
-      if (!hasKey) {
-        properties[label] = {selected: 'label'}
-      }
-      properties[label].keys = keys
-    })
-    return properties
-  }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/851dcb1a/zeppelin-web/src/app/tabledata/networkdata.test.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/tabledata/networkdata.test.js b/zeppelin-web/src/app/tabledata/networkdata.test.js
index f8d98a8..739ac19 100644
--- a/zeppelin-web/src/app/tabledata/networkdata.test.js
+++ b/zeppelin-web/src/app/tabledata/networkdata.test.js
@@ -35,12 +35,33 @@ describe('NetworkData build', function() {
       msg: JSON.stringify(jsonExpected)
     })
 
-    expect(nd.columns.length).toBe(2)
+    expect(nd.columns.length).toBe(1)
     expect(nd.rows.length).toBe(3)
     expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id)
     expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id)
     expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id)
-    expect(nd.graph.edges[0].source.id).toBe(jsonExpected.nodes[1].id)
-    expect(nd.graph.edges[0].target.id).toBe(jsonExpected.nodes[0].id)
+    expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source)
+    expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target)
+  })
+
+  it('should able to show data fields source and target', function() {
+    let jsonExpected = {nodes: [{id: 1, data: {source: 'Source'}}, {id: 2, data: {target: 'Target'}}],
+      edges: [{source: 2, target: 1, id: 1, data: {source: 'Source Edge Data', target: 'Target Edge Data'}}]}
+    nd.loadParagraphResult({
+      type: DatasetType.NETWORK,
+      msg: JSON.stringify(jsonExpected)
+    })
+
+    expect(nd.columns.length).toBe(3)
+    expect(nd.rows.length).toBe(3)
+    expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id)
+    expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id)
+    expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id)
+    expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source)
+    expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target)
+    expect(nd.graph.nodes[0].data.source).toBe(jsonExpected.nodes[0].data.source)
+    expect(nd.graph.nodes[1].data.target).toBe(jsonExpected.nodes[1].data.target)
+    expect(nd.graph.edges[0].data.source).toBe(jsonExpected.edges[0].data.source)
+    expect(nd.graph.edges[0].data.target).toBe(jsonExpected.edges[0].data.target)
   })
 })

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/851dcb1a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js
index 506b1c5..46ee251 100644
--- a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js
+++ b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js
@@ -55,25 +55,36 @@ export default class NetworkVisualization extends Visualization {
       console.log('graph not found')
       return
     }
-    console.log('Render Graph Visualization')
+    if (!networkData.isRendered) {
+      networkData.isRendered = true
+    } else {
+      return
+    }
+    console.log('Rendering the graph')
 
-    let transformationConfig = this.transformation.getSetting().scope.config
+    if (networkData.graph.edges.length &&
+        !networkData.isDefaultSet) {
+      networkData.isDefaultSet = true
+      this._setEdgesDefaults(networkData.graph)
+    }
+
+    const transformationConfig = this.transformation.getSetting().scope.config
     console.log('cfg', transformationConfig)
     if (transformationConfig && angular.equals({}, transformationConfig.properties)) {
-      transformationConfig.properties = networkData.getNetworkProperties()
+      transformationConfig.properties = this.getNetworkProperties(networkData.graph)
     }
 
     this.targetEl.empty().append('<svg></svg>')
 
-    let width = this.targetEl.width()
-    let height = this.targetEl.height()
-    let self = this
-    let defaultOpacity = 0
-    let nodeSize = 10
-    let textOffset = 3
-    let linkSize = 10
+    const width = this.targetEl.width()
+    const height = this.targetEl.height()
+    const self = this
+    const defaultOpacity = 0
+    const nodeSize = 10
+    const textOffset = 3
+    const linkSize = 10
 
-    let arcPath = (leftHand, d) => {
+    const arcPath = (leftHand, d) => {
       let start = leftHand ? d.source : d.target
       let end = leftHand ? d.target : d.source
       let dx = end.x - start.x
@@ -84,7 +95,7 @@ export default class NetworkVisualization extends Visualization {
       return `M${start.x},${start.y}A${dr},${dr} 0 0,${sweep} ${end.x},${end.y}`
     }
     // Use elliptical arc path segments to doubly-encode directionality.
-    let tick = () => {
+    const tick = () => {
       // Links
       linkPath.attr('d', function(d) {
         return arcPath(true, d)
@@ -97,7 +108,7 @@ export default class NetworkVisualization extends Visualization {
       text.attr('transform', (d) => `translate(${d.x},${d.y})`)
     }
 
-    let setOpacity = (scale) => {
+    const setOpacity = (scale) => {
       let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0
       this.svg.selectAll('.nodeLabel')
         .style('opacity', opacity)
@@ -105,7 +116,7 @@ export default class NetworkVisualization extends Visualization {
         .style('opacity', opacity)
     }
 
-    let zoom = d3.behavior.zoom()
+    const zoom = d3.behavior.zoom()
       .scaleExtent([1, 10])
       .on('zoom', () => {
         console.log('zoom')
@@ -135,13 +146,15 @@ export default class NetworkVisualization extends Visualization {
       })
       .start()
 
-    let renderFooterOnClick = (entity, type) => {
-      let footerId = this.containerId + '_footer'
-      let obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type}
-      let html = [this.$interpolate(['<li><b>{{type}}_id:</b>&nbsp{{id}}</li>',
-        '<li><b>{{type}}_type:</b>&nbsp{{label}}</li>'].join(''))(obj)]
+    const renderFooterOnClick = (entity, type) => {
+      const footerId = this.containerId + '_footer'
+      const obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type}
+      let html = [`<li><b>${obj.type}_id:</b>&nbsp${obj.id}</li>`]
+      if (obj.label) {
+        html.push(`<li><b>${obj.type}_type:</b>&nbsp${obj.label}</li>`)
+      }
       html = html.concat(_.map(entity.data, (v, k) => {
-        return this.$interpolate('<li><b>{{field}}:</b>&nbsp{{value}}</li>')({field: k, value: v})
+        return `<li><b>${k}:</b>&nbsp${v}</li>`
       }))
       angular.element('#' + footerId)
         .find('.list-inline')
@@ -149,7 +162,7 @@ export default class NetworkVisualization extends Visualization {
         .append(html.join(''))
     }
 
-    let drag = d3.behavior.drag()
+    const drag = d3.behavior.drag()
       .origin((d) => d)
       .on('dragstart', function(d) {
         console.log('dragstart')
@@ -171,7 +184,7 @@ export default class NetworkVisualization extends Visualization {
         self.force.resume()
       })
 
-    let container = this.svg.append('g')
+    const container = this.svg.append('g')
     if (networkData.graph.directed) {
       container.append('svg:defs').selectAll('marker')
         .data(['arrowMarker-' + this.containerId])
@@ -188,7 +201,7 @@ export default class NetworkVisualization extends Visualization {
         .attr('d', 'M0,-5L10,0L0,5')
     }
     // Links
-    let link = container.append('svg:g')
+    const link = container.append('svg:g')
       .on('click', () => {
         renderFooterOnClick(d3.select(d3.event.target).datum(), 'edge')
       })
@@ -196,13 +209,13 @@ export default class NetworkVisualization extends Visualization {
       .data(self.force.links())
       .enter()
       .append('g')
-    let getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count
-    let showLabel = (d) => this._showNodeLabel(d)
-    let linkPath = link.append('svg:path')
+    const getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count
+    const showLabel = (d) => this._showNodeLabel(d)
+    const linkPath = link.append('svg:path')
       .attr('class', 'link')
       .attr('size', linkSize)
       .attr('marker-end', `url(#arrowMarker-${this.containerId})`)
-    let textPath = link.append('svg:path')
+    const textPath = link.append('svg:path')
       .attr('id', getPathId)
       .attr('class', 'textpath')
     container.append('svg:g')
@@ -218,7 +231,7 @@ export default class NetworkVisualization extends Visualization {
       .text((d) => d.label)
       .style('opacity', defaultOpacity)
     // Nodes
-    let circle = container.append('svg:g')
+    const circle = container.append('svg:g')
       .on('click', () => {
         renderFooterOnClick(d3.select(d3.event.target).datum(), 'node')
       })
@@ -229,7 +242,7 @@ export default class NetworkVisualization extends Visualization {
       .attr('fill', (d) => networkData.graph.labels && d.label in networkData.graph.labels
                   ? networkData.graph.labels[d.label] : '#000000')
       .call(drag)
-    let text = container.append('svg:g').selectAll('g')
+    const text = container.append('svg:g').selectAll('g')
       .data(self.force.nodes())
       .enter().append('svg:g')
     text.append('svg:text')
@@ -252,12 +265,72 @@ export default class NetworkVisualization extends Visualization {
   }
 
   _showNodeLabel(d) {
-    let transformationConfig = this.transformation.getSetting().scope.config
-    let selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected
+    const transformationConfig = this.transformation.getSetting().scope.config
+    const selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected
     return d.data[selectedLabel] || d[selectedLabel]
   }
 
   getTransformation() {
     return this.transformation
   }
+
+  setNodesDefaults() {
+  }
+
+  _setEdgesDefaults(graph) {
+    graph.edges
+      .sort((a, b) => {
+        if (a.source > b.source) {
+          return 1
+        } else if (a.source < b.source) {
+          return -1
+        } else if (a.target > b.target) {
+          return 1
+        } else if (a.target < b.target) {
+          return -1
+        } else {
+          return 0
+        }
+      })
+    graph.edges
+      .forEach((edge, index) => {
+        let prevEdge = graph.edges[index - 1]
+        edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target
+            ? prevEdge.count : 0) + 1
+        edge.totalCount = graph.edges
+          .filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target)
+          .length
+      })
+    graph.edges
+      .forEach((edge) => {
+        if (typeof +edge.source === 'number') {
+          // edge.source = graph.nodes.filter((node) => +edge.source === +node.id)[0] || null
+          edge.source = _.find(graph.nodes, (node) => +edge.source === +node.id)
+        }
+        if (typeof +edge.target === 'number') {
+          // edge.target = graph.nodes.filter((node) => +edge.target === +node.id)[0] || null
+          edge.target = _.find(graph.nodes, (node) => +edge.target === +node.id)
+        }
+      })
+  }
+
+  getNetworkProperties(graph) {
+    const baseCols = ['id', 'label']
+    const properties = {}
+    graph.nodes.forEach(function(node) {
+      const hasLabel = 'label' in node && node.label !== ''
+      if (!hasLabel) {
+        return
+      }
+      const label = node.label
+      const hasKey = hasLabel && label in properties
+      const keys = _.uniq(Object.keys(node.data || {})
+              .concat(hasKey ? properties[label].keys : baseCols))
+      if (!hasKey) {
+        properties[label] = {selected: 'label'}
+      }
+      properties[label].keys = keys
+    })
+    return properties
+  }
 }