diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html index eab741b26cb..a45d5ad4c4d 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html @@ -59,6 +59,11 @@ ng-click="setGraphMode('map', true)" tooltip="Map" tooltip-placement="bottom"> + + Maps require internet connectivity. + + + + + diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-pivot.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-pivot.html index 9ddf9820640..e3d1d682619 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-pivot.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-pivot.html @@ -32,7 +32,7 @@ - + Keys @@ -213,4 +213,71 @@ + + + + + Source + + + + {{paragraph.config.graph.forceLayout.source.name}} + + + + + + + + Source Group + + + + {{paragraph.config.graph.forceLayout.sourceGroup.name}} + + + + + + + + Destination + + + + {{paragraph.config.graph.forceLayout.dest.name}} + + + + + + + + Destination Group + + + + {{paragraph.config.graph.forceLayout.destGroup.name}} + + + + + + diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index bd3b6b3c945..2993d46559f 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -1054,7 +1054,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r }; var setD3Chart = function(type, data, refresh) { - if (!$scope.chart[type]) { + if (!$scope.chart[type] && type !== 'forceLayout') { var chart = nv.models[type](); $scope.chart[type] = chart; } @@ -1087,6 +1087,8 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r $scope.chart[type].showDistX(true) .showDistY(true); //handle the problem of tooltip not showing when muliple points have same value. + } else if (type === 'forceLayout') { + var forceLayoutData = setForceLayoutData(data, 1000); } else { var p = pivot(data); if (type === 'pieChart') { @@ -1158,10 +1160,150 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r nv.utils.windowResize($scope.chart[type].update); }; + var renderForceLayout = function() { + var color = d3.scale.category10(); + var r = 5; + var margin = {top: -5, right: -5, bottom: -5, left: -5}; + var height = $scope.paragraph.config.graph.height; + var width = angular.element('#p' + $scope.paragraph.id + '_' + 'resize').width(); + + function zoomed() { + container.attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); + } + + function dragstarted(d) { + d3.event.sourceEvent.stopPropagation(); + /*jshint validthis:true */ + d3.select(this).classed('dragging', true); + force.start(); + } + + function dragged(d) { + /*jshint validthis:true */ + d3.select(this).attr('cx', d.x = d3.event.x).attr('cy', d.y = d3.event.y); + } + + function dragended(d) { + /*jshint validthis:true */ + d3.select(this).classed('dragging', false); + } + + d3.select('#p' + $scope.paragraph.id + '_' + type + ' svg g').remove(); + d3.select('#p' + $scope.paragraph.id + '_' + type + ' svg text').remove(); + d3.select('#p' + $scope.paragraph.id + '_' + type + ' div.tooltip').remove(); + + if (forceLayoutData) { + var force = d3.layout.force() + .charge(-120) + .linkDistance(30) + .size([width, height]); + + var zoom = d3.behavior.zoom() + .scaleExtent([0, 10]) + .on('zoom', zoomed); + + var drag = d3.behavior.drag() + .origin(function(d) { return d; }) + .on('dragstart', dragstarted) + .on('drag', dragged) + .on('dragend', dragended); + + var svg = d3.select('#p' + $scope.paragraph.id + '_' + type + ' svg') + .attr('width', width) + .attr('height', height) + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.right + ')') + .call(zoom); + + var tooltip = d3.select('#p' + $scope.paragraph.id + '_' + type).append('div') + .attr('class', 'tooltip') + .style('opacity', 0); + + svg.append('rect') + .attr('width', width) + .attr('height', height) + .style('fill', 'none') + .style('pointer-events', 'all'); + + var container = svg.append('g') + .attr('class', 'container'); + + force.nodes(forceLayoutData.nodes) + .links(forceLayoutData.links) + .start(); + + var link = container.append('g') + .attr('class', 'links') + .selectAll('.link') + .data(forceLayoutData.links) + .enter().append('line') + .attr('class', 'link') + .style('stroke-width', function(d) { return Math.sqrt(d.value); }); + + var node = container.append('g') + .attr('class', 'nodes') + .selectAll('.node') + .data(forceLayoutData.nodes) + .enter().append('g') + .attr('class', 'node') + .attr('cx', function(d) { return d.x; }) + .attr('cy', function(d) { return d.y; }) + .call(drag); + + node.append('circle') + .attr('r', r) + .style('fill', function(d) { return color(d.group); }); + + node.on('mouseover', function(d) { + var offsetX = d3.transform(container.attr('transform')).translate[0]; + var offsetY = d3.transform(container.attr('transform')).translate[1]; + var scale = d3.transform(container.attr('transform')).scale[0]; + tooltip.transition() + .duration(200) + .style('opacity', 0.9); + tooltip.html('name: ' + d.name + '' + + 'group: ' + d.group + '') + .style('left', (d.px * scale + offsetX + 15) + 'px') + .style('top', (d.py * scale + offsetY - 18) + 'px'); + }) + .on('mouseout', function(d) { + tooltip.transition() + .duration(200) + .style('opacity', 0); + }); + + force.on('tick', function() { + link.attr('x1', function(d) { return d.source.x; }) + .attr('y1', function(d) { return d.source.y; }) + .attr('x2', function(d) { return d.target.x; }) + .attr('y2', function(d) { return d.target.y; }); + + node.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; }); + }); + } else { + // create 'No available data' svg message instead of graph when no data is available + var svg = d3.select('#p' + $scope.paragraph.id + '_' + type + ' svg') + .attr('width', width) + .attr('height', height) + .append('text') + .attr('x', width / 2) + .attr('y', height / 2) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .style('font-size', '18px') + .style('font-weight', 'bold') + .text('No available data (choose fields in settings)'); + } + }; + var retryRenderer = function() { if (angular.element('#p' + $scope.paragraph.id + '_' + type + ' svg').length !== 0) { try { - renderChart(); + if (type !== 'forceLayout') { + renderChart(); + } else { + renderForceLayout(); + } } catch (err) { console.log('Chart drawing error %o', err); } @@ -1482,6 +1624,30 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); }; + $scope.removeForceLayoutOptionSource = function(idx) { + $scope.paragraph.config.graph.forceLayout.source = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeForceLayoutOptionSourceGroup = function(idx) { + $scope.paragraph.config.graph.forceLayout.sourceGroup = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeForceLayoutOptionDest = function(idx) { + $scope.paragraph.config.graph.forceLayout.dest = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeForceLayoutOptionDestGroup = function(idx) { + $scope.paragraph.config.graph.forceLayout.destGroup = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + /* Clear unknown columns from graph option */ var clearUnknownColsFromGraphOption = function() { var unique = function(list) { @@ -2034,6 +2200,38 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r }; }; + var setForceLayoutData = function(data, limit) { + try { + var source = $scope.paragraph.config.graph.forceLayout.source; + var dest = $scope.paragraph.config.graph.forceLayout.dest; + var sourceGroup = $scope.paragraph.config.graph.forceLayout.sourceGroup; + var destGroup = $scope.paragraph.config.graph.forceLayout.destGroup; + var nodes = []; + var links = []; + var nodesMap = {}; + angular.forEach(data.rows, function(row, index) { + if (index < limit) { + if (!(row[source.index] in nodesMap)) { + nodesMap[row[source.index]] = nodes.length; + nodes.push({name: row[source.index], group: row[sourceGroup.index]}); + } + if (!(row[dest.index] in nodesMap)) { + nodesMap[row[dest.index]] = nodes.length; + nodes.push({name: row[dest.index], group: row[destGroup.index]}); + } + } + }); + angular.forEach(data.rows, function(row, index) { + if (index < limit) { + links.push({source: nodesMap[row[source.index]], target: nodesMap[row[dest.index]], value: 1}); + } + }); + return {nodes: nodes, links: links}; + } catch (e) { + console.log(e.name + ': ' + e.message); + } + }; + var isDiscrete = function(field) { var getUnique = function(f) { var uniqObj = {}; diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css index 0b821c94181..f46a4f66ebe 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css @@ -528,3 +528,45 @@ table.table-striped { padding-left: 4px !important; width: 20px; } + +/* + Force Layout CSS +*/ + +.node { + stroke: #999; + stroke-width: 1.5px; + } + +.link { + stroke: #999; + stroke-opacity: .6; +} + +.forceLayout div.tooltip { + position: absolute; + text-align: center; + width: auto; + padding: 4px 6px 4px 6px; + font: 12px sans-serif; + background: rgba(0, 0, 0, 0.75); + color: white; + border: 0px; + border-radius: 4px; + pointer-events: none; +} + +.forceLayout div.tooltip:before { + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; + border-right: 6px solid rgba(0, 0, 0, 0.75); + content: ""; + position: absolute; + z-index: 99; + left: -6px; + top: 11px; +} + +.forceLayout .tooltip-text { + color: orangered; +}