+
+
\ No newline at end of file
diff --git a/zeppelin-web/src/app/tabledata/networkdata.js b/zeppelin-web/src/app/tabledata/networkdata.js
new file mode 100644
index 00000000000..7983d827265
--- /dev/null
+++ b/zeppelin-web/src/app/tabledata/networkdata.js
@@ -0,0 +1,145 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import TableData from './tabledata'
+import {DatasetType} from './dataset'
+
+/**
+ * Create network data object from paragraph graph type result
+ */
+export default class NetworkData extends TableData {
+ constructor(graph) {
+ super()
+ this.graph = graph || {}
+ if (this.graph.nodes) {
+ this.loadParagraphResult({msg: JSON.stringify(graph), type: DatasetType.NETWORK})
+ }
+ }
+
+ loadParagraphResult(paragraphResult) {
+ if (!paragraphResult || paragraphResult.type !== DatasetType.NETWORK) {
+ console.log('Can not load paragraph result')
+ return
+ }
+
+ this.graph = JSON.parse(paragraphResult.msg.trim() || '{}')
+
+ if (!this.graph.nodes) {
+ console.log('Graph result is empty')
+ return
+ }
+
+ this.setNodesDefaults()
+ this.setEdgesDefaults()
+
+ 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 || {}) })
+ keys = _.flatten(keys)
+ keys = _.uniq(keys).filter(function(key) {
+ return baseCols.indexOf(key) === -1
+ })
+ let columnNames = baseColumnNames.concat(_.map(keys, function(elem, i) {
+ return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'}
+ }))
+ for (let i = 0; i < entities.length; i++) {
+ let entity = entities[i]
+ let col = []
+ let col2 = []
+ 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
+ ? entity[name] : entity.data[name]
+ let parsedValue = value === null || value === undefined ? '' : value
+ col.push(parsedValue)
+ col2.push({key: name, value: parsedValue})
+ }
+ rows.push(col)
+ }
+
+ this.comment = comment
+ 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
+ }
+}
diff --git a/zeppelin-web/src/app/tabledata/networkdata.test.js b/zeppelin-web/src/app/tabledata/networkdata.test.js
new file mode 100644
index 00000000000..f8d98a89a3f
--- /dev/null
+++ b/zeppelin-web/src/app/tabledata/networkdata.test.js
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import NetworkData from './networkdata.js'
+import {DatasetType} from './dataset.js'
+
+describe('NetworkData build', function() {
+ let nd
+
+ beforeEach(function() {
+ nd = new NetworkData()
+ })
+
+ it('should initialize the default value', function() {
+ expect(nd.columns.length).toBe(0)
+ expect(nd.rows.length).toBe(0)
+ expect(nd.graph).toEqual({})
+ })
+
+ it('should able to create NetowkData from paragraph result', function() {
+ let jsonExpected = {nodes: [{id: 1}, {id: 2}], edges: [{source: 2, target: 1, id: 1}]}
+ nd.loadParagraphResult({
+ type: DatasetType.NETWORK,
+ msg: JSON.stringify(jsonExpected)
+ })
+
+ expect(nd.columns.length).toBe(2)
+ 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)
+ })
+})
diff --git a/zeppelin-web/src/app/tabledata/tabledata.js b/zeppelin-web/src/app/tabledata/tabledata.js
index 8e4e6b6625e..3fe01b7791c 100644
--- a/zeppelin-web/src/app/tabledata/tabledata.js
+++ b/zeppelin-web/src/app/tabledata/tabledata.js
@@ -11,19 +11,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Dataset, DatasetType} from './dataset'
/**
* Create table data object from paragraph table type result
*/
-export default class TableData {
+export default class TableData extends Dataset {
constructor (columns, rows, comment) {
+ super()
this.columns = columns || []
this.rows = rows || []
this.comment = comment || ''
}
loadParagraphResult (paragraphResult) {
- if (!paragraphResult || paragraphResult.type !== 'TABLE') {
+ if (!paragraphResult || paragraphResult.type !== DatasetType.TABLE) {
console.log('Can not load paragraph result')
return
}
diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js
new file mode 100644
index 00000000000..506b1c5f186
--- /dev/null
+++ b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js
@@ -0,0 +1,263 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Visualization from '../visualization'
+import NetworkTransformation from '../../tabledata/network'
+
+/**
+ * Visualize data in network format
+ */
+export default class NetworkVisualization extends Visualization {
+ constructor(targetEl, config) {
+ super(targetEl, config)
+ console.log('Init network viz')
+ if (!config.properties) {
+ config.properties = {}
+ }
+ if (!config.d3Graph) {
+ config.d3Graph = {
+ forceLayout: {
+ timeout: 10000,
+ charge: -120,
+ linkDistance: 80,
+ },
+ zoom: {
+ minScale: 1.3
+ }
+ }
+ }
+ this.targetEl.addClass('network')
+ this.containerId = this.targetEl.prop('id')
+ this.force = null
+ this.svg = null
+ this.$timeout = angular.injector(['ng']).get('$timeout')
+ this.$interpolate = angular.injector(['ng']).get('$interpolate')
+ this.transformation = new NetworkTransformation(config)
+ }
+
+ refresh() {
+ console.log('refresh')
+ }
+
+ render(networkData) {
+ if (!('graph' in networkData)) {
+ console.log('graph not found')
+ return
+ }
+ console.log('Render Graph Visualization')
+
+ let transformationConfig = this.transformation.getSetting().scope.config
+ console.log('cfg', transformationConfig)
+ if (transformationConfig && angular.equals({}, transformationConfig.properties)) {
+ transformationConfig.properties = networkData.getNetworkProperties()
+ }
+
+ this.targetEl.empty().append('')
+
+ 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
+
+ let arcPath = (leftHand, d) => {
+ let start = leftHand ? d.source : d.target
+ let end = leftHand ? d.target : d.source
+ let dx = end.x - start.x
+ let dy = end.y - start.y
+ let dr = d.totalCount === 1
+ ? 0 : Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) / (1 + (1 / d.totalCount) * (d.count - 1))
+ let sweep = leftHand ? 0 : 1
+ 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 = () => {
+ // Links
+ linkPath.attr('d', function(d) {
+ return arcPath(true, d)
+ })
+ textPath.attr('d', function(d) {
+ return arcPath(d.source.x < d.target.x, d)
+ })
+ // Nodes
+ circle.attr('transform', (d) => `translate(${d.x},${d.y})`)
+ text.attr('transform', (d) => `translate(${d.x},${d.y})`)
+ }
+
+ let setOpacity = (scale) => {
+ let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0
+ this.svg.selectAll('.nodeLabel')
+ .style('opacity', opacity)
+ this.svg.selectAll('textPath')
+ .style('opacity', opacity)
+ }
+
+ let zoom = d3.behavior.zoom()
+ .scaleExtent([1, 10])
+ .on('zoom', () => {
+ console.log('zoom')
+ setOpacity(d3.event.scale)
+ container.attr('transform', `translate(${d3.event.translate})scale(${d3.event.scale})`)
+ })
+
+ this.svg = d3.select(`#${this.containerId} svg`)
+ .attr('width', width)
+ .attr('height', height)
+ .call(zoom)
+
+ this.force = d3.layout.force()
+ .charge(transformationConfig.d3Graph.forceLayout.charge)
+ .linkDistance(transformationConfig.d3Graph.forceLayout.linkDistance)
+ .on('tick', tick)
+ .nodes(networkData.graph.nodes)
+ .links(networkData.graph.edges)
+ .size([width, height])
+ .on('start', () => {
+ console.log('force layout start')
+ this.$timeout(() => { this.force.stop() }, transformationConfig.d3Graph.forceLayout.timeout)
+ })
+ .on('end', () => {
+ console.log('force layout stop')
+ setOpacity(zoom.scale())
+ })
+ .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(['
> **Note :** Display system is backend independent.
+
+## Network
+
+With the `%network` directive, Zeppelin treats your output as a graph. Zeppelin can leverage the Property Graph Model.
+
+### What is the Labelled Property Graph Model?
+
+A [Property Graph](https://github.com/tinkerpop/gremlin/wiki/Defining-a-Property-Graph) is a graph that has these elements:
+
+* a set of vertices
+ * each vertex has a unique identifier.
+ * each vertex has a set of outgoing edges.
+ * each vertex has a set of incoming edges.
+ * each vertex has a collection of properties defined by a map from key to value
+* a set of edges
+ * each edge has a unique identifier.
+ * each edge has an outgoing tail vertex.
+ * each edge has an incoming head vertex.
+ * each edge has a label that denotes the type of relationship between its two vertices.
+ * each edge has a collection of properties defined by a map from key to value.
+
+
+
+A [Labelled Property Graph](https://neo4j.com/developer/graph-database/#property-graph) is a Property Graph where the nodes can be tagged with **labels** representing their different roles in the graph model
+
+
+
+### What are the APIs?
+
+The new NETWORK visualization is based on json with the following params:
+
+* "nodes" (mandatory): list of nodes of the graph every node can have the following params:
+ * "id" (mandatory): the id of the node (must be unique);
+ * "label": the main Label of the node;
+ * "labels": the list of the labels of the node;
+ * "data": the data attached to the node;
+* "edges": list of the edges of the graph;
+ * "id" (mandatory): the id of the edge (must be unique);
+ * "source" (mandatory): the id of source node of the edge;
+ * "target" (mandatory): the id of target node of the edge;
+ * "label": the main type of the edge;
+ * "data": the data attached to the edge;
+* "labels": a map (K, V) where K is the node label and V is the color of the node;
+* "directed": (true/false, default false) wich tells if is directed graph or not;
+* "types": a *distinct* list of the edge types of the graph
+
+If you click on a node or edge on the bottom of the paragraph you find a list of entity properties
+
+
+
+This kind of graph can be easily *flatten* in order to support other visualization formats provided by Zeppelin.
+
+
+
+### How to use it?
+
+An example of a simple graph
+
+```
+%spark
+print(s"""
+%network {
+ "nodes": [
+ {"id": 1},
+ {"id": 2},
+ {"id": 3}
+ ],
+ "edges": [
+ {"source": 1, "target": 2, "id" : 1},
+ {"source": 2, "target": 3, "id" : 2},
+ {"source": 1, "target": 2, "id" : 3},
+ {"source": 1, "target": 2, "id" : 4},
+ {"source": 2, "target": 1, "id" : 5},
+ {"source": 2, "target": 1, "id" : 6}
+ ]
+}
+""")
+```
+
+that will look like:
+
+
+
+A little more complex graph:
+
+```
+%spark
+print(s"""
+%network {
+ "nodes": [{"id": 1, "label": "User", "data": {"fullName":"Andrea Santurbano"}},{"id": 2, "label": "User", "data": {"fullName":"Lee Moon Soo"}},{"id": 3, "label": "Project", "data": {"name":"Zeppelin"}}],
+ "edges": [{"source": 2, "target": 1, "id" : 1, "label": "HELPS"},{"source": 2, "target": 3, "id" : 2, "label": "CREATE"},{"source": 1, "target": 3, "id" : 3, "label": "CONTRIBUTE_TO", "data": {"oldPR": "https://github.com/apache/zeppelin/pull/1582"}}],
+ "labels": {"User": "#8BC34A", "Project": "#3071A9"},
+ "directed": true,
+ "types": ["HELPS", "CREATE", "CONTRIBUTE_TO"]
+}
+""")
+```
+
+that will look like:
+
+
\ No newline at end of file
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResult.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResult.java
index 23164906626..7aab8a3e1e8 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResult.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResult.java
@@ -50,7 +50,8 @@ public static enum Type {
TABLE,
IMG,
SVG,
- NULL
+ NULL,
+ NETWORK
}
Code code;
diff --git a/zeppelin-web/.eslintrc b/zeppelin-web/.eslintrc
index 1fe3fa5e7de..6dca5c8982b 100644
--- a/zeppelin-web/.eslintrc
+++ b/zeppelin-web/.eslintrc
@@ -58,6 +58,7 @@
"no-undef": 2,
"no-unused-vars": [2, { "vars": "local", "args": "none" }],
"strict": [2, "global"],
- "max-len": [2, {"code": 120, "ignoreComments": true, "ignoreRegExpLiterals": true}]
+ "max-len": [2, {"code": 120, "ignoreComments": true, "ignoreRegExpLiterals": true}],
+ "linebreak-style": 0
}
}
diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css
index b17acf74fb3..b63794d2bc2 100644
--- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css
+++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css
@@ -557,3 +557,7 @@ table.table-striped {
.markdown-body h4 {
font-size: 16px;
}
+
+.network-labels {
+ margin: 0.2em;
+}
diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html b/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html
index 4becdc6f22a..9592d809d34 100644
--- a/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html
+++ b/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html
@@ -13,12 +13,13 @@
-->