Rebase 30/04

This commit is contained in:
conker84 2017-04-30 16:06:28 +02:00
parent c3b7087463
commit 6e74eb9f32
28 changed files with 1246 additions and 22 deletions

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View file

@ -61,3 +61,56 @@ 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" />

View file

@ -50,7 +50,8 @@ public class InterpreterResult implements Serializable {
TABLE,
IMG,
SVG,
NULL
NULL,
NETWORK
}
Code code;

View file

@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.zeppelin.interpreter.graph;
import java.util.Map;
/**
* The base network entity
*
*/
public abstract class GraphEntity {
private long id;
/**
* The data of the entity
*
*/
private Map<String, Object> data;
/**
* The primary type of the entity
*/
private String label;
//private String color;
public GraphEntity() {}
public GraphEntity(long id, Map<String, Object> data, String label) {
super();
this.setId(id);
this.setData(data);
this.setLabel(label);
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Map<String, Object> getData() {
return data;
}
public void setData(Map<String, Object> data) {
this.data = data;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}

View file

@ -0,0 +1,120 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.zeppelin.interpreter.graph;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.apache.zeppelin.interpreter.InterpreterResult;
import com.google.gson.Gson;
/**
* The intepreter result template for Networks
*
*/
public class GraphResult extends InterpreterResult {
/**
* The Graph structure parsed from the front-end
*
*/
public static class Graph {
private Collection<Node> nodes;
private Collection<Relationship> edges;
/**
* The node types in the whole graph, and the related colors
*
*/
private Map<String, String> labels;
/**
* The relationship types in the whole graph
*
*/
private Set<String> types;
/**
* Is a directed graph
*/
private boolean directed;
public Graph() {}
public Graph(Collection<Node> nodes, Collection<Relationship> edges,
Map<String, String> labels, Set<String> types, boolean directed) {
super();
this.setNodes(nodes);
this.setEdges(edges);
this.setLabels(labels);
this.setTypes(types);
this.setDirected(directed);
}
public Collection<Node> getNodes() {
return nodes;
}
public void setNodes(Collection<Node> nodes) {
this.nodes = nodes;
}
public Collection<Relationship> getEdges() {
return edges;
}
public void setEdges(Collection<Relationship> edges) {
this.edges = edges;
}
public Map<String, String> getLabels() {
return labels;
}
public void setLabels(Map<String, String> labels) {
this.labels = labels;
}
public Set<String> getTypes() {
return types;
}
public void setTypes(Set<String> types) {
this.types = types;
}
public boolean isDirected() {
return directed;
}
public void setDirected(boolean directed) {
this.directed = directed;
}
}
private static final Gson gson = new Gson();
public GraphResult(Code code, Graph graphObject) {
super(code, Type.NETWORK, gson.toJson(graphObject));
}
}

View file

@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.zeppelin.interpreter.graph;
/**
* An utiliy class for networks
*
*/
public class GraphUtils {
private GraphUtils() {}
private static final String[] LETTERS = "0123456789ABCDEF".split("");
public static final String COLOR_GREY = "#D3D3D3";
public static String getRandomColor() {
char[] color = new char[7];
color[0] = '#';
for (int i = 1; i < color.length; i++) {
color[i] = LETTERS[(int) Math.floor(Math.random() * 16)].charAt(0);
}
return new String(color);
}
}

View file

@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.zeppelin.interpreter.graph;
import java.util.Map;
import java.util.Set;
/**
* The Zeppelin Node Entity
*
*/
public class Node extends GraphEntity {
/**
* The labels (types) attached to a node
*/
private Set<String> labels;
public Node() {}
public Node(long id, Map<String, Object> data, Set<String> labels) {
super(id, data, labels.iterator().next());
}
public Set<String> getLabels() {
return labels;
}
public void setLabels(Set<String> labels) {
this.labels = labels;
}
}

View file

@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.zeppelin.interpreter.graph;
import java.util.Map;
/**
* The Zeppelin Relationship entity
*
*/
public class Relationship extends GraphEntity {
/**
* Source node ID
*/
private long source;
/**
* End node ID
*/
private long target;
public Relationship() {}
public Relationship(long id, Map<String, Object> data, long source,
long target, String label, int count) {
super(id, data, label);
this.setSource(source);
this.setTarget(target);
}
public Relationship(long id, Map<String, Object> data, long source,
long target, String label) {
this(id, data, source, target, label, 0);
}
public long getSource() {
return source;
}
public void setSource(long startNodeId) {
this.source = startNodeId;
}
public long getTarget() {
return target;
}
public void setTarget(long endNodeId) {
this.target = endNodeId;
}
}

View file

@ -0,0 +1,38 @@
package org.apache.zeppelin.interpreter.graph;
import static org.junit.Assert.assertEquals;
import org.apache.zeppelin.interpreter.InterpreterResult;
import org.apache.zeppelin.interpreter.graph.GraphResult.Graph;
import org.junit.Test;
import com.google.gson.Gson;
public class GraphResultTest {
@Test
public void testGraphResult() {
final String expected = "{\"nodes\":[{\"id\":1,\"label\":\"User\",\"data\":{\"fullname\":\"Andrea Santurbano\"}},"
+ "{\"id\":2,\"label\":\"User\",\"data\":{\"fullname\":\"Moon soo Lee\"}}],"
+ "\"edges\":[{\"source\":2,\"target\":1,\"id\":1,\"label\":\"HELPS\",\"data\":{\"project\":\"Zeppelin\",\"githubUrl\":\"https://github.com/apache/zeppelin/pull/1582\"}}]}";
Graph graphExpected = new Gson().fromJson(expected, Graph.class);
GraphResult intepreterResult = new GraphResult(InterpreterResult.Code.SUCCESS, graphExpected);
assertEquals("The type is NETWORK", InterpreterResult.Type.NETWORK, intepreterResult.message().get(0).getType());
Graph resultGraph = new Gson().fromJson(intepreterResult.toString().replace("%network ", ""), Graph.class);
assertEquals("Nodes must have the same size", graphExpected.getNodes().size(), resultGraph.getNodes().size());
assertEquals("Edges must have the same size", graphExpected.getEdges().size(), resultGraph.getEdges().size());
Node nodeSourceExpected = graphExpected.getNodes().iterator().next();
Node nodeTargetExpected = graphExpected.getNodes().iterator().next();
Relationship relExpected = graphExpected.getEdges().iterator().next();
Node nodeSourceResult = resultGraph.getNodes().iterator().next();
Node nodeTargetResult = resultGraph.getNodes().iterator().next();
Relationship relResult = resultGraph.getEdges().iterator().next();
assertEquals("Nodes source must have the same id", nodeSourceExpected.getId(), nodeSourceResult.getId());
assertEquals("Nodes target must have the same id", nodeTargetExpected.getId(), nodeTargetResult.getId());
assertEquals("Edges must have the same id", relExpected.getId(), relResult.getId());
assertEquals("Edges must have the same id", relExpected.getId(), relResult.getId());
}
}

View file

@ -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
}
}

View file

@ -557,3 +557,7 @@ table.table-striped {
.markdown-body h4 {
font-size: 16px;
}
.network-labels {
margin: 0.2em;
}

View file

@ -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>

View file

@ -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)

View file

@ -57,3 +57,31 @@
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;
}

View file

@ -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"

View file

@ -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 {

View file

@ -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}

View file

@ -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')
}
}
}

View file

@ -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'))
})
})

View file

@ -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
}
}

View file

@ -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">Minumin 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>

View file

@ -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
}
}

View file

@ -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)
})
})

View file

@ -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
}

View file

@ -0,0 +1,264 @@
/*
* 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>&nbsp{{id}}</li>',
'<li><b>{{type}}_type:</b>&nbsp{{label}}</li>'].join(''))(obj)]
html = html.concat(_.map(entity.data, (v, k) => {
return this.$interpolate('<li><b>{{field}}:</b>&nbsp{{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
}
}

View file

@ -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 {