Repository: zeppelin Updated Branches: refs/heads/master c8e2d9fa3 -> 43926485c
[ZEPPELIN-2222] Add Network Visualization ### What is this PR for? This issue is about a new network visualization that can leverage the Property Graph Model (https://github.com/tinkerpop/gremlin/wiki/Defining-a-Property-Graph), but also simple graphs in order to provide a set of base apis that can be used by graph dbs (like Neo4j) or graph processing frameworks (like GraphX or Giraph). ### What type of PR is it? [Feature] Is related to the #1582 ### Todos * [x] - Added the intepreter apis to manage graphs (under the pakage **org.apache.zeppelin.interpreter.graph**) * [x] - Added the frontend apis to manage graphs (via d3js) ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2222 ### How should this be tested? You can download [this notebook](https://gist.github.com/conker84/9574127c2389d08164423894aa93b67f) to test the PR ### Screenshots (if appropriate) ![zeppelin-pr-screen](https://cloud.githubusercontent.com/assets/1833335/23830683/b883e916-0710-11e7-980d-c8ab6bf6d26b.PNG) ### Video ![zeppelin](https://cloud.githubusercontent.com/assets/1833335/23902630/1e121a0c-08c2-11e7-9f28-134866dba077.gif) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? Yes Author: conker84 <sant...@gmail.com> Closes #2125 from conker84/master and squashes the following commits: b6062a0b0 [conker84] Removed package org.apache.zeppelin.interpreter.graph e98ca7a67 [conker84] Comments of review 14/03/2017 b31b7b7ac [conker84] Rebase of 07/04/2017 3257bea24 [conker84] Rebase 30/4 6e74eb9f3 [conker84] Rebase 30/04 Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/43926485 Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/43926485 Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/43926485 Branch: refs/heads/master Commit: 43926485cd022871374ab0421a1fec75492e9526 Parents: c8e2d9f Author: conker84 <sant...@gmail.com> Authored: Thu Jun 8 14:48:01 2017 +0200 Committer: Jongyoul Lee <jongy...@apache.org> Committed: Fri Jun 9 15:37:43 2017 +0900 ---------------------------------------------------------------------- docs/_includes/themes/zeppelin/_navigation.html | 1 + .../img/screenshots/display_complex_network.png | Bin 0 -> 48817 bytes .../img/screenshots/display_network.png | Bin 0 -> 52981 bytes .../img/screenshots/display_network_flatten.png | Bin 0 -> 42001 bytes .../img/screenshots/display_simple_network.png | Bin 0 -> 31928 bytes docs/displaysystem/basicdisplaysystem.md | 101 +++++++ .../zeppelin/interpreter/InterpreterResult.java | 3 +- zeppelin-web/.eslintrc | 3 +- .../src/app/notebook/paragraph/paragraph.css | 4 + .../paragraph/result/result-chart-selector.html | 11 +- .../paragraph/result/result.controller.js | 44 +++- .../app/notebook/paragraph/result/result.css | 56 ++++ .../app/notebook/paragraph/result/result.html | 28 +- zeppelin-web/src/app/spell/spell-result.js | 2 + zeppelin-web/src/app/tabledata/dataset.js | 36 +++ .../src/app/tabledata/datasetfactory.js | 33 +++ .../src/app/tabledata/datasetfactory.test.js | 46 ++++ zeppelin-web/src/app/tabledata/network.js | 48 ++++ .../src/app/tabledata/network_settings.html | 74 ++++++ zeppelin-web/src/app/tabledata/networkdata.js | 145 ++++++++++ .../src/app/tabledata/networkdata.test.js | 46 ++++ zeppelin-web/src/app/tabledata/tabledata.js | 6 +- .../builtins/visualization-d3network.js | 263 +++++++++++++++++++ .../components/resizable/resizable.directive.js | 2 +- 24 files changed, 930 insertions(+), 22 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/docs/_includes/themes/zeppelin/_navigation.html ---------------------------------------------------------------------- diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index 296be25..dd710d0 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -88,6 +88,7 @@ <li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#text">Text</a></li> <li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#html">Html</a></li> <li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#table">Table</a></li> + <li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#network">Network</a></li> <li role="separator" class="divider"></li> <li class="title"><span><b>Angular API</b><span></li> <li><a href="{{BASE_PATH}}/displaysystem/back-end-angular.html">Angular (backend API)</a></li> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/docs/assets/themes/zeppelin/img/screenshots/display_complex_network.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/screenshots/display_complex_network.png b/docs/assets/themes/zeppelin/img/screenshots/display_complex_network.png new file mode 100644 index 0000000..c788c26 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/display_complex_network.png differ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/docs/assets/themes/zeppelin/img/screenshots/display_network.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/screenshots/display_network.png b/docs/assets/themes/zeppelin/img/screenshots/display_network.png new file mode 100644 index 0000000..516c853 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/display_network.png differ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/docs/assets/themes/zeppelin/img/screenshots/display_network_flatten.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/screenshots/display_network_flatten.png b/docs/assets/themes/zeppelin/img/screenshots/display_network_flatten.png new file mode 100644 index 0000000..743e666 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/display_network_flatten.png differ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/docs/assets/themes/zeppelin/img/screenshots/display_simple_network.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/screenshots/display_simple_network.png b/docs/assets/themes/zeppelin/img/screenshots/display_simple_network.png new file mode 100644 index 0000000..86a08fe Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/display_simple_network.png differ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/docs/displaysystem/basicdisplaysystem.md ---------------------------------------------------------------------- diff --git a/docs/displaysystem/basicdisplaysystem.md b/docs/displaysystem/basicdisplaysystem.md index 7c42432..15cefef 100644 --- a/docs/displaysystem/basicdisplaysystem.md +++ b/docs/displaysystem/basicdisplaysystem.md @@ -61,3 +61,104 @@ If table contents start with `%html`, it is interpreted as an HTML. <img src="../assets/themes/zeppelin/img/screenshots/display_table_html.png" /> > **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. + +<img src="https://github.com/tinkerpop/gremlin/raw/master/doc/images/graph-example-1.jpg" /> + +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 + +<img src="http://s3.amazonaws.com/dev.assets.neo4j.com/wp-content/uploads/property_graph_model.png" /> + +### 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 + +<img src="../assets/themes/zeppelin/img/screenshots/display_network.png" /> + +This kind of graph can be easily *flatten* in order to support other visualization formats provided by Zeppelin. + +<img src="../assets/themes/zeppelin/img/screenshots/display_network_flatten.png" /> + +### 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: + +<img src="../assets/themes/zeppelin/img/screenshots/display_simple_network.png" /> + +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: + +<img src="../assets/themes/zeppelin/img/screenshots/display_complex_network.png" /> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResult.java ---------------------------------------------------------------------- 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 2316490..7aab8a3 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 class InterpreterResult implements Serializable { TABLE, IMG, SVG, - NULL + NULL, + NETWORK } Code code; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/.eslintrc ---------------------------------------------------------------------- diff --git a/zeppelin-web/.eslintrc b/zeppelin-web/.eslintrc index 1fe3fa5..6dca5c8 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 } } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/notebook/paragraph/paragraph.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css index 1a5e992..bebea60 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css @@ -560,3 +560,7 @@ table.table-striped { .markdown-body h4 { font-size: 16px; } + +.network-labels { + margin: 0.2em; +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html ---------------------------------------------------------------------- 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 4becdc6..9592d80 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 @@ limitations under the License. --> <div id="{{id}}_switch" - ng-if="(type == 'TABLE' || apps.length > 0 || suggestion.available && suggestion.available.length > 0) && !asIframe && !viewOnly" + ng-if="(type == 'TABLE' || type == 'NETWORK' || apps.length > 0 || suggestion.available && suggestion.available.length > 0) && !asIframe && !viewOnly" class="result-chart-selector"> - <div ng-if="type == 'TABLE'" class="btn-group"> + <div ng-if="type == 'TABLE' || type == 'NETWORK'" class="btn-group"> <button type="button" class="btn btn-default btn-sm" ng-repeat="viz in builtInTableDataVisualizationList track by $index" + ng-if="viz.supports.indexOf(type) > -1" ng-class="{'active' : viz.id == graphMode && !config.helium.activeApp}" ng-click="switchViz(viz.id)" tooltip-placement="bottom" uib-tooltip="{{viz.name ? viz.name : ''}}" @@ -28,7 +29,7 @@ limitations under the License. <div class="btn-group"> <button type="button" - ng-if="type != 'TABLE'" + ng-if="type != 'TABLE' && type != 'NETWORK'" ng-click="switchApp()" ng-class="{'active' : !config.helium.activeApp}" class="btn btn-default btn-sm"><i class="fa fa-terminal"></i> @@ -73,7 +74,7 @@ limitations under the License. </div> <div class="btn-group" - ng-if="type == 'TABLE' && !asIframe && !viewOnly" + ng-if="(type == 'TABLE' || type == 'NETWORK') && !asIframe && !viewOnly" style="margin-bottom: 10px;"> <button type="button" class="btn btn-default btn-sm" style="margin-left:10px" @@ -93,7 +94,7 @@ limitations under the License. </div> <span - ng-if="type=='TABLE' && !config.helium.activeApp && !asIframe && !viewOnly" + ng-if="(type == 'TABLE' || type == 'NETWORK') && !config.helium.activeApp && graphMode!='table' && !asIframe && !viewOnly" style="margin-left:10px; cursor:pointer; display: inline-block; vertical-align:top; position: relative; line-height:30px;"> <a class="btnText" ng-click="toggleGraphSetting()"> settings <span ng-class="config.graph.optionOpen ? 'fa fa-caret-up' : 'fa fa-caret-down'"></span> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index 8237c78..c2ec7f2 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -14,13 +14,14 @@ import moment from 'moment' -import TableData from '../../../tabledata/tabledata' +import DatasetFactory from '../../../tabledata/datasetfactory' import TableVisualization from '../../../visualization/builtins/visualization-table' import BarchartVisualization from '../../../visualization/builtins/visualization-barchart' import PiechartVisualization from '../../../visualization/builtins/visualization-piechart' import AreachartVisualization from '../../../visualization/builtins/visualization-areachart' import LinechartVisualization from '../../../visualization/builtins/visualization-linechart' import ScatterchartVisualization from '../../../visualization/builtins/visualization-scatterchart' +import NetworkVisualization from '../../../visualization/builtins/visualization-d3network' import { DefaultDisplayType, SpellResult, @@ -44,36 +45,48 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio { id: 'table', // paragraph.config.graph.mode name: 'Table', // human readable name. tooltip - icon: '<i class="fa fa-table"></i>' + icon: '<i class="fa fa-table"></i>', + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] }, { id: 'multiBarChart', name: 'Bar Chart', icon: '<i class="fa fa-bar-chart"></i>', - transformation: 'pivot' + transformation: 'pivot', + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] }, { id: 'pieChart', name: 'Pie Chart', icon: '<i class="fa fa-pie-chart"></i>', - transformation: 'pivot' + transformation: 'pivot', + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] }, { id: 'stackedAreaChart', name: 'Area Chart', icon: '<i class="fa fa-area-chart"></i>', - transformation: 'pivot' + transformation: 'pivot', + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] }, { id: 'lineChart', name: 'Line Chart', icon: '<i class="fa fa-line-chart"></i>', - transformation: 'pivot' + transformation: 'pivot', + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] }, { id: 'scatterChart', name: 'Scatter Chart', - icon: '<i class="cf cf-scatter-chart"></i>' + icon: '<i class="cf cf-scatter-chart"></i>', + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] + }, + { + id: 'network', + name: 'Network', + icon: '<i class="fa fa-share-alt"></i>', + supports: [DefaultDisplayType.NETWORK] } ] @@ -104,6 +117,10 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio 'scatterChart': { class: ScatterchartVisualization, instance: undefined + }, + 'network': { + class: NetworkVisualization, + instance: undefined } } @@ -253,18 +270,23 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio // enable only when it is last result enableHelium = (index === paragraphRef.results.msg.length - 1) - if ($scope.type === 'TABLE') { - tableData = new TableData() + if ($scope.type === 'TABLE' || $scope.type === 'NETWORK') { + tableData = new DatasetFactory().createDataset($scope.type) tableData.loadParagraphResult({type: $scope.type, msg: data}) $scope.tableDataColumns = tableData.columns $scope.tableDataComment = tableData.comment + if ($scope.type === 'NETWORK') { + $scope.networkNodes = tableData.networkNodes + $scope.networkRelationships = tableData.networkRelationships + $scope.networkProperties = tableData.networkProperties + } } else if ($scope.type === 'IMG') { $scope.imageData = data } } $scope.createDisplayDOMId = function (baseDOMId, type) { - if (type === DefaultDisplayType.TABLE) { + if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) { return `${baseDOMId}_graph` } else if (type === DefaultDisplayType.HTML) { return `${baseDOMId}_html` @@ -281,7 +303,7 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio $scope.renderDefaultDisplay = function (targetElemId, type, data, refresh) { const afterLoaded = () => { - if (type === DefaultDisplayType.TABLE) { + if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) { renderGraph(targetElemId, $scope.graphMode, refresh) } else if (type === DefaultDisplayType.HTML) { renderHtml(targetElemId, data) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/notebook/paragraph/result/result.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.css b/zeppelin-web/src/app/notebook/paragraph/result/result.css index 3e864ef..9f9b967 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.css +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.css @@ -57,3 +57,59 @@ font-weight: 400; text-align: center; } + +/* D3 Graph Configuration */ +marker { + fill: #D3D3D3; +} +path.link { + fill: none; + stroke-width: 3px; + stroke: #D3D3D3; +} +path.textpath { + fill: none; + stroke: none; +} + +text { + font-size: 12px; + pointer-events: none; +} +text.shadow { + stroke: #fff; + stroke-width: 3px; + stroke-opacity: .8; +} +text.nodeLabel { + font-size: 1em; + pointer-events: none; +} + +/* D3 Graph Configuration */ +marker { + fill: #D3D3D3; +} +path.link { + fill: none; + stroke-width: 3px; + stroke: #D3D3D3; +} +path.textpath { + fill: none; + stroke: none; +} + +text { + font-size: 12px; + pointer-events: none; +} +text.shadow { + stroke: #fff; + stroke-width: 3px; + stroke-opacity: .8; +} +text.nodeLabel { + font-size: 1em; + pointer-events: none; +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/notebook/paragraph/result/result.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.html b/zeppelin-web/src/app/notebook/paragraph/result/result.html index faac9cd..5099cfa 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.html +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.html @@ -23,7 +23,7 @@ limitations under the License. resize='{"allowresize": "{{!asIframe && !viewOnly}}", "graphType": "{{type}}"}' resizable on-resize="resize(width, height);"> - <div ng-if="type=='TABLE'" + <div ng-if="type=='TABLE' || type == 'NETWORK'" ng-style="getPointerEvent()"> <!-- setting --> <div class="option lightBold" style="overflow: visible;" @@ -36,6 +36,26 @@ limitations under the License. ng-show="graphMode == viz.id"></div> </div> + <div id="p{{id}}_network_header" + ng-if="type == 'NETWORK' && graphMode == 'network' && networkNodes != null"> + <ul class="list-inline"> + <li>Nodes <span class="badge">{{networkNodes.count}}</span>:</li> + <li ng-repeat="(labelName, labelColor) in networkNodes.labels" style="padding: 0"> + <span style="background-color: {{labelColor}} !important;" class="label label-default network-badge"> + {{labelName}} + </span> + </li> + </ul> + <ul class="list-inline"> + <li ng-if="networkRelationships != null">Relationships <span class="badge">{{networkRelationships.count}}</span>:</li> + <li ng-repeat="type in networkRelationships.types" style="padding: 0"> + <span class="label label-default network-badge"> + {{type}} + </span> + </li> + </ul> + </div> + <!-- graph --> <div id="p{{id}}_graph" class="graphContainer" @@ -46,6 +66,12 @@ limitations under the License. </div> </div> + <div id="p{{id}}_network_footer" + ng-if="type == 'NETWORK' && graphMode == 'network'"> + <ul class="list-inline"> + </ul> + </div> + <div id="{{id}}_comment" class="text" ng-if="tableDataComment" http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/spell/spell-result.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/spell/spell-result.js b/zeppelin-web/src/app/spell/spell-result.js index e8e2a0a..5ba65c2 100644 --- a/zeppelin-web/src/app/spell/spell-result.js +++ b/zeppelin-web/src/app/spell/spell-result.js @@ -21,6 +21,7 @@ export const DefaultDisplayType = { HTML: 'HTML', ANGULAR: 'ANGULAR', TEXT: 'TEXT', + NETWORK: 'NETWORK' } export const DefaultDisplayMagic = { @@ -29,6 +30,7 @@ export const DefaultDisplayMagic = { '%html': DefaultDisplayType.HTML, '%angular': DefaultDisplayType.ANGULAR, '%text': DefaultDisplayType.TEXT, + '%network': DefaultDisplayType.NETWORK, } export class DataWithType { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/tabledata/dataset.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/dataset.js b/zeppelin-web/src/app/tabledata/dataset.js new file mode 100644 index 0000000..762e300 --- /dev/null +++ b/zeppelin-web/src/app/tabledata/dataset.js @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/** + * The abstract dataset rapresentation + */ +class Dataset { + /** + * Load the paragraph result, every Dataset implementation must override this method + * where is contained the business rules to convert the paragraphResult object to the related dataset type + */ + loadParagraphResult(paragraphResult) { + // override this + } +} + +/** + * Dataset types + */ +const DatasetType = Object.freeze({ + NETWORK: 'NETWORK', + TABLE: 'TABLE' +}) + +export {Dataset, DatasetType} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/tabledata/datasetfactory.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/datasetfactory.js b/zeppelin-web/src/app/tabledata/datasetfactory.js new file mode 100644 index 0000000..f2f69c9 --- /dev/null +++ b/zeppelin-web/src/app/tabledata/datasetfactory.js @@ -0,0 +1,33 @@ +/* + * 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 NetworkData from './networkdata' +import {DatasetType} from './dataset' + +/** + * Create table data object from paragraph table type result + */ +export default class DatasetFactory { + createDataset(datasetType) { + switch (datasetType) { + case DatasetType.NETWORK: + return new NetworkData() + case DatasetType.TABLE: + return new TableData() + default: + throw new Error('Dataset type not found') + } + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/tabledata/datasetfactory.test.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/datasetfactory.test.js b/zeppelin-web/src/app/tabledata/datasetfactory.test.js new file mode 100644 index 0000000..0beb137 --- /dev/null +++ b/zeppelin-web/src/app/tabledata/datasetfactory.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 TableData from './tabledata.js' +import {DatasetType} from './dataset.js' +import DatasetFactory from './datasetfactory.js' + +describe('DatasetFactory build', function() { + let df + + beforeAll(function() { + df = new DatasetFactory() + }) + + it('should create a TableData instance', function() { + let td = df.createDataset(DatasetType.TABLE) + expect(td instanceof TableData).toBeTruthy() + expect(td.columns.length).toBe(0) + expect(td.rows.length).toBe(0) + }) + + it('should create a NetworkData instance', function() { + let nd = df.createDataset(DatasetType.NETWORK) + expect(nd instanceof NetworkData).toBeTruthy() + expect(nd.columns.length).toBe(0) + expect(nd.rows.length).toBe(0) + expect(nd.graph).toEqual({}) + }) + + it('should thrown an Error', function() { + expect(function() { df.createDataset('text') }) + .toThrow(new Error('Dataset type not found')) + }) +}) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/tabledata/network.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/network.js b/zeppelin-web/src/app/tabledata/network.js new file mode 100644 index 0000000..403ea5b --- /dev/null +++ b/zeppelin-web/src/app/tabledata/network.js @@ -0,0 +1,48 @@ +/* + * 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 Transformation from './transformation' + +/** + * trasformation settings for network visualization + */ +export default class NetworkTransformation extends Transformation { + getSetting() { + let self = this + let configObj = self.config + return { + template: 'app/tabledata/network_settings.html', + scope: { + config: configObj, + isEmptyObject: function(obj) { + obj = obj || {} + return angular.equals(obj, {}) + }, + setNetworkLabel: function(label, value) { + configObj.properties[label].selected = value + }, + saveConfig: function() { + self.emitConfig(configObj) + } + } + } + } + + setConfig(config) { + } + + transform(networkData) { + return networkData + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/tabledata/network_settings.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/network_settings.html b/zeppelin-web/src/app/tabledata/network_settings.html new file mode 100644 index 0000000..ad1daa9 --- /dev/null +++ b/zeppelin-web/src/app/tabledata/network_settings.html @@ -0,0 +1,74 @@ +<!-- +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. +--> +<div class="row"> + <form> + <fieldset class=" col-xs-12"> + <h4>Force Layout settings</h4> + <div class="form-check col-xs-4"> + <label for="{{$id}}_timeout">Stop Force layout after</label> + <div class="input-group"> + <input type="text" class="form-control" ng-model="config.d3Graph.forceLayout.timeout" id="{{$id}}_timeout" /> + <span class="input-group-addon">ms</span> + </div> + </div> + <div class="form-check col-xs-4"> + <div class="form-group"> + <label for="{{$id}}_charge">Force Layout Charge</label> + <input type="text" class="form-control" ng-model="config.d3Graph.forceLayout.charge" id="{{$id}}_charge" /> + </div> + </div> + <div class="form-check col-xs-4"> + <div class="form-group"> + <label for="{{$id}}_linkDistance">Force Layout Link Distance</label> + <input type="text" class="form-control" ng-model="config.d3Graph.forceLayout.linkDistance" id="{{$id}}_linkDistance" /> + </div> + </div> + </fieldset> + + <fieldset class=" col-xs-12"> + <h4>Globals</h4> + <div class="form-check col-xs-4"> + <div class="form-group"> + <label for="{{$id}}_charge">Minimum scale to show node and edge labels</label> + <input type="text" class="form-control" ng-model="config.d3Graph.zoom.minScale" id="{{$id}}_minScale" /> + </div> + </div> + </fieldset> + + <fieldset class="form-group col-xs-12"> + <h4>Choose node labels</h4> + <div ng-if="isEmptyObject(config.properties)"> + No labels to set + </div> + <div class="btn-group network-labels network-badge-settings" + ng-repeat="(key, value) in config.properties track by key"> + <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + {{key}}:<i>{{config.properties[key].selected}}</i> <div class="caret"></div> + </button> + <ul class="dropdown-menu"> + <li ng-repeat="val in value.keys" ng-click="setNetworkLabel(key, val)"> + <a><i ng-if="config.properties[key].selected == val" class="glyphicon glyphicon-ok"> + </i> {{val}}</a> + </li> + </ul> + </div> + </fieldset> + <fieldset class="form-group col-xs-12"> + <button type="submit" class="btn btn-primary btn-sm" ng-click="saveConfig()"> + <span class="glyphicon glyphicon-floppy-disk"></span> + Save configuration + </button> + </fieldset> + </form> +</div> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/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 new file mode 100644 index 0000000..7983d82 --- /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 + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/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 new file mode 100644 index 0000000..f8d98a8 --- /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) + }) +}) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/app/tabledata/tabledata.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/tabledata.js b/zeppelin-web/src/app/tabledata/tabledata.js index 8e4e6b6..3fe01b7 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 } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/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 new file mode 100644 index 0000000..506b1c5 --- /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('<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 + + 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(['<li><b>{{type}}_id:</b> {{id}}</li>', + '<li><b>{{type}}_type:</b> {{label}}</li>'].join(''))(obj)] + html = html.concat(_.map(entity.data, (v, k) => { + return this.$interpolate('<li><b>{{field}}:</b> {{value}}</li>')({field: k, value: v}) + })) + angular.element('#' + footerId) + .find('.list-inline') + .empty() + .append(html.join('')) + } + + let drag = d3.behavior.drag() + .origin((d) => d) + .on('dragstart', function(d) { + console.log('dragstart') + d3.event.sourceEvent.stopPropagation() + d3.select(this).classed('dragging', true) + self.force.stop() + }) + .on('drag', function(d) { + console.log('drag') + d.px += d3.event.dx + d.py += d3.event.dy + d.x += d3.event.dx + d.y += d3.event.dy + }) + .on('dragend', function(d) { + console.log('dragend') + d.fixed = true + d3.select(this).classed('dragging', false) + self.force.resume() + }) + + let container = this.svg.append('g') + if (networkData.graph.directed) { + container.append('svg:defs').selectAll('marker') + .data(['arrowMarker-' + this.containerId]) + .enter() + .append('svg:marker') + .attr('id', String) + .attr('viewBox', '0 -5 10 10') + .attr('refX', 16) + .attr('refY', 0) + .attr('markerWidth', 4) + .attr('markerHeight', 4) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5') + } + // Links + let link = container.append('svg:g') + .on('click', () => { + renderFooterOnClick(d3.select(d3.event.target).datum(), 'edge') + }) + .selectAll('g.link') + .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') + .attr('class', 'link') + .attr('size', linkSize) + .attr('marker-end', `url(#arrowMarker-${this.containerId})`) + let textPath = link.append('svg:path') + .attr('id', getPathId) + .attr('class', 'textpath') + container.append('svg:g') + .selectAll('.pathLabel') + .data(self.force.links()) + .enter() + .append('svg:text') + .attr('class', 'pathLabel') + .append('svg:textPath') + .attr('startOffset', '50%') + .attr('text-anchor', 'middle') + .attr('xlink:href', (d) => '#' + getPathId(d)) + .text((d) => d.label) + .style('opacity', defaultOpacity) + // Nodes + let circle = container.append('svg:g') + .on('click', () => { + renderFooterOnClick(d3.select(d3.event.target).datum(), 'node') + }) + .selectAll('circle') + .data(self.force.nodes()) + .enter().append('svg:circle') + .attr('r', (d) => nodeSize) + .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') + .data(self.force.nodes()) + .enter().append('svg:g') + text.append('svg:text') + .attr('x', (d) => nodeSize + textOffset) + .attr('size', nodeSize) + .attr('y', '.31em') + .attr('class', (d) => 'nodeLabel shadow label-' + d.label) + .text(showLabel) + .style('opacity', defaultOpacity) + text.append('svg:text') + .attr('x', (d) => nodeSize + textOffset) + .attr('size', nodeSize) + .attr('y', '.31em') + .attr('class', (d) => 'nodeLabel label-' + d.label) + .text(showLabel) + .style('opacity', defaultOpacity) + } + + destroy() { + } + + _showNodeLabel(d) { + let transformationConfig = this.transformation.getSetting().scope.config + let selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected + return d.data[selectedLabel] || d[selectedLabel] + } + + getTransformation() { + return this.transformation + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/43926485/zeppelin-web/src/components/resizable/resizable.directive.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/resizable/resizable.directive.js b/zeppelin-web/src/components/resizable/resizable.directive.js index f0eed76..7bf8477 100644 --- a/zeppelin-web/src/components/resizable/resizable.directive.js +++ b/zeppelin-web/src/components/resizable/resizable.directive.js @@ -35,7 +35,7 @@ function resizable () { let colStep = window.innerWidth / 12 elem.off('resizestop') let conf = angular.copy(resizableConfig) - if (resize.graphType === 'TABLE' || resize.graphType === 'TEXT') { + if (resize.graphType === 'TABLE' || resize.graphType === 'NETWORK' || resize.graphType === 'TEXT') { conf.grid = [colStep, 10] conf.minHeight = 100 } else {