Repository: zeppelin Updated Branches: refs/heads/master 53e6f743d -> 851dcb1a3
[ZEPPELIN-3038] Network visualization not show "source" and "target" node/edge properties ### What is this PR for? The Network visualization not show "source" and "target" node/edge properties when the graph is flattened to create the table representation. ### What type of PR is it? [Bug Fix] ### Todos * [x] - Fixed ### Screenshot Before:  After:  ### What is the Jira issue? [ZEPPELIN-3038](https://issues.apache.org/jira/browse/ZEPPELIN-3038) ### How should this be tested? Please use this [notebook](https://gist.github.com/conker84/9574127c2389d08164423894aa93b67f) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Andrea Santurbano <sant...@gmail.com> Closes #2653 from conker84/network-fix and squashes the following commits: d4f19b6 [Andrea Santurbano] removed unused property 44100c6 [Andrea Santurbano] Removed semicolons 273c88f [Andrea Santurbano] added test on edges prevent double rendering 5eeabc2 [Andrea Santurbano] networkdata class now shows fields "source" and "target" + added test case moved the logic related to the visualization from networkdata to visualization-d3network added some optimization Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/851dcb1a Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/851dcb1a Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/851dcb1a Branch: refs/heads/master Commit: 851dcb1a392a3b720501982c6c2c4a3d30468a7e Parents: 53e6f74 Author: Andrea Santurbano <sant...@gmail.com> Authored: Tue Nov 7 18:39:58 2017 +0100 Committer: Felix Cheung <felixche...@apache.org> Committed: Sat Dec 23 10:34:39 2017 -0800 ---------------------------------------------------------------------- zeppelin-web/src/app/tabledata/networkdata.js | 104 +++----------- .../src/app/tabledata/networkdata.test.js | 27 +++- .../builtins/visualization-d3network.js | 135 ++++++++++++++----- 3 files changed, 150 insertions(+), 116 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/851dcb1a/zeppelin-web/src/app/tabledata/networkdata.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/networkdata.js b/zeppelin-web/src/app/tabledata/networkdata.js index 7983d82..70cd86b 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.js +++ b/zeppelin-web/src/app/tabledata/networkdata.js @@ -40,42 +40,40 @@ export default class NetworkData extends TableData { return } - this.setNodesDefaults() - this.setEdgesDefaults() - + this.graph.edges = this.graph.edges || [] this.networkNodes = angular.equals({}, this.graph.labels || {}) ? null : {count: this.graph.nodes.length, labels: this.graph.labels} this.networkRelationships = angular.equals([], this.graph.types || []) ? null : {count: this.graph.edges.length, types: this.graph.types} - let rows = [] - let comment = '' - let entities = this.graph.nodes.concat(this.graph.edges) - let baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'}, - {name: 'label', index: 1, aggr: 'sum'}] - let internalFieldsToJump = ['count', 'size', 'totalCount', - 'data', 'x', 'y', 'labels'] - let baseCols = _.map(baseColumnNames, function(col) { return col.name }) - let keys = _.map(entities, function(elem) { return Object.keys(elem.data || {}) }) + const rows = [] + const comment = '' + const entities = this.graph.nodes.concat(this.graph.edges) + const baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'}] + const containsLabelField = _.find(entities, (entity) => 'label' in entity) != null + if (this.graph.labels || this.graph.types || containsLabelField) { + baseColumnNames.push({name: 'label', index: 1, aggr: 'sum'}) + } + const internalFieldsToJump = ['count', 'size', 'totalCount', + 'data', 'x', 'y', 'labels', 'source', 'target'] + const baseCols = _.map(baseColumnNames, (col) => col.name) + let keys = _.map(entities, (elem) => Object.keys(elem.data || {})) keys = _.flatten(keys) - keys = _.uniq(keys).filter(function(key) { - return baseCols.indexOf(key) === -1 - }) - let columnNames = baseColumnNames.concat(_.map(keys, function(elem, i) { + keys = _.uniq(keys).filter((key) => baseCols.indexOf(key) === -1) + const entityColumnNames = _.map(keys, (elem, i) => { return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'} - })) + }) + const columnNames = baseColumnNames.concat(entityColumnNames) for (let i = 0; i < entities.length; i++) { - let entity = entities[i] - let col = [] - let col2 = [] + const entity = entities[i] + const col = [] entity.data = entity.data || {} for (let j = 0; j < columnNames.length; j++) { - let name = columnNames[j].name - let value = name in entity && internalFieldsToJump.indexOf(name) === -1 + const name = columnNames[j].name + const value = name in entity && internalFieldsToJump.indexOf(name) === -1 ? entity[name] : entity.data[name] - let parsedValue = value === null || value === undefined ? '' : value + const parsedValue = value === null || value === undefined ? '' : value col.push(parsedValue) - col2.push({key: name, value: parsedValue}) } rows.push(col) } @@ -84,62 +82,4 @@ export default class NetworkData extends TableData { this.columns = columnNames this.rows = rows } - - setNodesDefaults() { - } - - setEdgesDefaults() { - this.graph.edges - .sort((a, b) => { - if (a.source > b.source) { - return 1 - } else if (a.source < b.source) { - return -1 - } else if (a.target > b.target) { - return 1 - } else if (a.target < b.target) { - return -1 - } else { - return 0 - } - }) - this.graph.edges - .forEach((edge, index) => { - let prevEdge = this.graph.edges[index - 1] - edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target - ? prevEdge.count : 0) + 1 - edge.totalCount = this.graph.edges - .filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target) - .length - }) - this.graph.edges - .forEach((edge) => { - if (typeof +edge.source === 'number') { - edge.source = this.graph.nodes.filter((node) => +edge.source === +node.id)[0] || null - } - if (typeof +edge.target === 'number') { - edge.target = this.graph.nodes.filter((node) => +edge.target === +node.id)[0] || null - } - }) - } - - getNetworkProperties() { - let baseCols = ['id', 'label'] - let properties = {} - this.graph.nodes.forEach(function(node) { - let hasLabel = 'label' in node && node.label !== '' - if (!hasLabel) { - return - } - let label = node.label - let hasKey = hasLabel && label in properties - let keys = _.uniq(Object.keys(node.data || {}) - .concat(hasKey ? properties[label].keys : baseCols)) - if (!hasKey) { - properties[label] = {selected: 'label'} - } - properties[label].keys = keys - }) - return properties - } } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/851dcb1a/zeppelin-web/src/app/tabledata/networkdata.test.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/tabledata/networkdata.test.js b/zeppelin-web/src/app/tabledata/networkdata.test.js index f8d98a8..739ac19 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.test.js +++ b/zeppelin-web/src/app/tabledata/networkdata.test.js @@ -35,12 +35,33 @@ describe('NetworkData build', function() { msg: JSON.stringify(jsonExpected) }) - expect(nd.columns.length).toBe(2) + expect(nd.columns.length).toBe(1) expect(nd.rows.length).toBe(3) expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id) expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id) expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id) - expect(nd.graph.edges[0].source.id).toBe(jsonExpected.nodes[1].id) - expect(nd.graph.edges[0].target.id).toBe(jsonExpected.nodes[0].id) + expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source) + expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target) + }) + + it('should able to show data fields source and target', function() { + let jsonExpected = {nodes: [{id: 1, data: {source: 'Source'}}, {id: 2, data: {target: 'Target'}}], + edges: [{source: 2, target: 1, id: 1, data: {source: 'Source Edge Data', target: 'Target Edge Data'}}]} + nd.loadParagraphResult({ + type: DatasetType.NETWORK, + msg: JSON.stringify(jsonExpected) + }) + + expect(nd.columns.length).toBe(3) + expect(nd.rows.length).toBe(3) + expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id) + expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id) + expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id) + expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source) + expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target) + expect(nd.graph.nodes[0].data.source).toBe(jsonExpected.nodes[0].data.source) + expect(nd.graph.nodes[1].data.target).toBe(jsonExpected.nodes[1].data.target) + expect(nd.graph.edges[0].data.source).toBe(jsonExpected.edges[0].data.source) + expect(nd.graph.edges[0].data.target).toBe(jsonExpected.edges[0].data.target) }) }) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/851dcb1a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js index 506b1c5..46ee251 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js @@ -55,25 +55,36 @@ export default class NetworkVisualization extends Visualization { console.log('graph not found') return } - console.log('Render Graph Visualization') + if (!networkData.isRendered) { + networkData.isRendered = true + } else { + return + } + console.log('Rendering the graph') - let transformationConfig = this.transformation.getSetting().scope.config + if (networkData.graph.edges.length && + !networkData.isDefaultSet) { + networkData.isDefaultSet = true + this._setEdgesDefaults(networkData.graph) + } + + const transformationConfig = this.transformation.getSetting().scope.config console.log('cfg', transformationConfig) if (transformationConfig && angular.equals({}, transformationConfig.properties)) { - transformationConfig.properties = networkData.getNetworkProperties() + transformationConfig.properties = this.getNetworkProperties(networkData.graph) } this.targetEl.empty().append('<svg></svg>') - let width = this.targetEl.width() - let height = this.targetEl.height() - let self = this - let defaultOpacity = 0 - let nodeSize = 10 - let textOffset = 3 - let linkSize = 10 + const width = this.targetEl.width() + const height = this.targetEl.height() + const self = this + const defaultOpacity = 0 + const nodeSize = 10 + const textOffset = 3 + const linkSize = 10 - let arcPath = (leftHand, d) => { + const arcPath = (leftHand, d) => { let start = leftHand ? d.source : d.target let end = leftHand ? d.target : d.source let dx = end.x - start.x @@ -84,7 +95,7 @@ export default class NetworkVisualization extends Visualization { return `M${start.x},${start.y}A${dr},${dr} 0 0,${sweep} ${end.x},${end.y}` } // Use elliptical arc path segments to doubly-encode directionality. - let tick = () => { + const tick = () => { // Links linkPath.attr('d', function(d) { return arcPath(true, d) @@ -97,7 +108,7 @@ export default class NetworkVisualization extends Visualization { text.attr('transform', (d) => `translate(${d.x},${d.y})`) } - let setOpacity = (scale) => { + const setOpacity = (scale) => { let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0 this.svg.selectAll('.nodeLabel') .style('opacity', opacity) @@ -105,7 +116,7 @@ export default class NetworkVisualization extends Visualization { .style('opacity', opacity) } - let zoom = d3.behavior.zoom() + const zoom = d3.behavior.zoom() .scaleExtent([1, 10]) .on('zoom', () => { console.log('zoom') @@ -135,13 +146,15 @@ export default class NetworkVisualization extends Visualization { }) .start() - let renderFooterOnClick = (entity, type) => { - let footerId = this.containerId + '_footer' - let obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type} - let html = [this.$interpolate(['<li><b>{{type}}_id:</b> {{id}}</li>', - '<li><b>{{type}}_type:</b> {{label}}</li>'].join(''))(obj)] + const renderFooterOnClick = (entity, type) => { + const footerId = this.containerId + '_footer' + const obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type} + let html = [`<li><b>${obj.type}_id:</b> ${obj.id}</li>`] + if (obj.label) { + html.push(`<li><b>${obj.type}_type:</b> ${obj.label}</li>`) + } html = html.concat(_.map(entity.data, (v, k) => { - return this.$interpolate('<li><b>{{field}}:</b> {{value}}</li>')({field: k, value: v}) + return `<li><b>${k}:</b> ${v}</li>` })) angular.element('#' + footerId) .find('.list-inline') @@ -149,7 +162,7 @@ export default class NetworkVisualization extends Visualization { .append(html.join('')) } - let drag = d3.behavior.drag() + const drag = d3.behavior.drag() .origin((d) => d) .on('dragstart', function(d) { console.log('dragstart') @@ -171,7 +184,7 @@ export default class NetworkVisualization extends Visualization { self.force.resume() }) - let container = this.svg.append('g') + const container = this.svg.append('g') if (networkData.graph.directed) { container.append('svg:defs').selectAll('marker') .data(['arrowMarker-' + this.containerId]) @@ -188,7 +201,7 @@ export default class NetworkVisualization extends Visualization { .attr('d', 'M0,-5L10,0L0,5') } // Links - let link = container.append('svg:g') + const link = container.append('svg:g') .on('click', () => { renderFooterOnClick(d3.select(d3.event.target).datum(), 'edge') }) @@ -196,13 +209,13 @@ export default class NetworkVisualization extends Visualization { .data(self.force.links()) .enter() .append('g') - let getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count - let showLabel = (d) => this._showNodeLabel(d) - let linkPath = link.append('svg:path') + const getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count + const showLabel = (d) => this._showNodeLabel(d) + const linkPath = link.append('svg:path') .attr('class', 'link') .attr('size', linkSize) .attr('marker-end', `url(#arrowMarker-${this.containerId})`) - let textPath = link.append('svg:path') + const textPath = link.append('svg:path') .attr('id', getPathId) .attr('class', 'textpath') container.append('svg:g') @@ -218,7 +231,7 @@ export default class NetworkVisualization extends Visualization { .text((d) => d.label) .style('opacity', defaultOpacity) // Nodes - let circle = container.append('svg:g') + const circle = container.append('svg:g') .on('click', () => { renderFooterOnClick(d3.select(d3.event.target).datum(), 'node') }) @@ -229,7 +242,7 @@ export default class NetworkVisualization extends Visualization { .attr('fill', (d) => networkData.graph.labels && d.label in networkData.graph.labels ? networkData.graph.labels[d.label] : '#000000') .call(drag) - let text = container.append('svg:g').selectAll('g') + const text = container.append('svg:g').selectAll('g') .data(self.force.nodes()) .enter().append('svg:g') text.append('svg:text') @@ -252,12 +265,72 @@ export default class NetworkVisualization extends Visualization { } _showNodeLabel(d) { - let transformationConfig = this.transformation.getSetting().scope.config - let selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected + const transformationConfig = this.transformation.getSetting().scope.config + const selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected return d.data[selectedLabel] || d[selectedLabel] } getTransformation() { return this.transformation } + + setNodesDefaults() { + } + + _setEdgesDefaults(graph) { + graph.edges + .sort((a, b) => { + if (a.source > b.source) { + return 1 + } else if (a.source < b.source) { + return -1 + } else if (a.target > b.target) { + return 1 + } else if (a.target < b.target) { + return -1 + } else { + return 0 + } + }) + graph.edges + .forEach((edge, index) => { + let prevEdge = graph.edges[index - 1] + edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target + ? prevEdge.count : 0) + 1 + edge.totalCount = graph.edges + .filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target) + .length + }) + graph.edges + .forEach((edge) => { + if (typeof +edge.source === 'number') { + // edge.source = graph.nodes.filter((node) => +edge.source === +node.id)[0] || null + edge.source = _.find(graph.nodes, (node) => +edge.source === +node.id) + } + if (typeof +edge.target === 'number') { + // edge.target = graph.nodes.filter((node) => +edge.target === +node.id)[0] || null + edge.target = _.find(graph.nodes, (node) => +edge.target === +node.id) + } + }) + } + + getNetworkProperties(graph) { + const baseCols = ['id', 'label'] + const properties = {} + graph.nodes.forEach(function(node) { + const hasLabel = 'label' in node && node.label !== '' + if (!hasLabel) { + return + } + const label = node.label + const hasKey = hasLabel && label in properties + const keys = _.uniq(Object.keys(node.data || {}) + .concat(hasKey ? properties[label].keys : baseCols)) + if (!hasKey) { + properties[label] = {selected: 'label'} + } + properties[label].keys = keys + }) + return properties + } }