mirror of
https://github.com/apache/zeppelin
synced 2026-05-24 09:38:26 +00:00
Rebase 30/04
This commit is contained in:
parent
c3b7087463
commit
6e74eb9f32
28 changed files with 1246 additions and 22 deletions
|
|
@ -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>
|
||||
|
|
|
|||
BIN
docs/assets/themes/zeppelin/img/screenshots/display_network.png
Normal file
BIN
docs/assets/themes/zeppelin/img/screenshots/display_network.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ public class InterpreterResult implements Serializable {
|
|||
TABLE,
|
||||
IMG,
|
||||
SVG,
|
||||
NULL
|
||||
NULL,
|
||||
NETWORK
|
||||
}
|
||||
|
||||
Code code;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -557,3 +557,7 @@ table.table-striped {
|
|||
.markdown-body h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.network-labels {
|
||||
margin: 0.2em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
36
zeppelin-web/src/app/tabledata/dataset.js
Normal file
36
zeppelin-web/src/app/tabledata/dataset.js
Normal 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}
|
||||
33
zeppelin-web/src/app/tabledata/datasetfactory.js
Normal file
33
zeppelin-web/src/app/tabledata/datasetfactory.js
Normal 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')
|
||||
}
|
||||
}
|
||||
}
|
||||
46
zeppelin-web/src/app/tabledata/datasetfactory.test.js
Normal file
46
zeppelin-web/src/app/tabledata/datasetfactory.test.js
Normal 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'))
|
||||
})
|
||||
})
|
||||
48
zeppelin-web/src/app/tabledata/network.js
Normal file
48
zeppelin-web/src/app/tabledata/network.js
Normal 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
|
||||
}
|
||||
}
|
||||
74
zeppelin-web/src/app/tabledata/network_settings.html
Normal file
74
zeppelin-web/src/app/tabledata/network_settings.html
Normal 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>
|
||||
145
zeppelin-web/src/app/tabledata/networkdata.js
Normal file
145
zeppelin-web/src/app/tabledata/networkdata.js
Normal 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
|
||||
}
|
||||
}
|
||||
46
zeppelin-web/src/app/tabledata/networkdata.test.js
Normal file
46
zeppelin-web/src/app/tabledata/networkdata.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {{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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue