I am trying to get the force directed graph example working, but the graph has committed a design faux pas and the tooltip blocks access to the element it is supposed to give more information about, creating a slow strobe effect.
I'm fine with turning them off or fixing them, but I don't know javascript very well. I tried to remove all reference to tooltips, but it still shows tooltips. I'm wondering if anyone has already fixed it.
Got it working by obliterating tooltips. It still shows node names on mouseover, but the giant tooltip is gone. I had to remove the mouseover effect being called from the d3 library. I also changed the CSS, but I don't know if that's germane to the problem.
Javascript:
// Force Directed Graphs!
// these require an input of (at least) 3 fields in the format
// 'stats count by field1 field2 field3'
// ---- settings ----
// height, width
// panAndZoom: the ability to zoom (true, false)
// directional: true, false
// valueField: what field to count by
// charges, gravity: change the look of the graph, play around with these!
// linkDistance: the distance between each node
// ---- expected data format ----
// a splunk search like this: source=*somedata* | stats count by artist_name track_name device
// each group is an artist/song pairing
// {
// "nodes":[
// {
// "source":"Bruno Mars",
// "group":0
// },
// {
// "source":"It Will Rain",
// "group":0
// },
// {
// "source":"Cobra Starship",
// "group":1
// },
// {
// "source":"You Make Me Feel",
// "group":1
// },
// {
// "source":"Gym Class Heroes",
// "group":2
// },
// {
// "source":"Stereo Hearts",
// "group":2
// },
// ],
// "links":[
// {
// "source":0,
// "target":1,
// "value":null
// },
// {
// "source":2,
// "target":3,
// "value":null
// },
// {
// "source":4,
// "target":5,
// "value":null
// },
// ],
// - we add this part -
// "groupNames":{
// "iphone":49,
// "android":53,
// "blackberry":48,
// "ipad":52,
// "ipod":50
// },
// "groupLookup":[
// "iphone",
// "android",
// "blackberry",
// "ipad",
// "ipod"
// ]
// }
define(function(require, exports, module) {
var _ = require('underscore');
var d3 = require("../d3/d3");
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
require("css!./forcedirected.css");
var ForceDirected = SimpleSplunkView.extend({
moduleId: module.id,
className: "splunk-toolkit-force-directed",
options: {
managerid: null,
data: 'preview',
panAndZoom: true,
directional: true,
valueField: 'count',
charges: -500,
gravity: 0.2,
linkDistance: 15,
swoop: false,
isStatic: true
},
output_mode: "json_rows",
initialize: function() {
SimpleSplunkView.prototype.initialize.apply(this, arguments);
// in the case that any options are changed, it will dynamically update
// without having to refresh.
this.settings.on("change:charges", this.render, this);
this.settings.on("change:gravity", this.render, this);
this.settings.on("change:linkDistance", this.render, this);
this.settings.on("change:directional", this.render, this);
this.settings.on("change:panAndZoom", this.render, this);
this.settings.on("change:swoop", this.render, this);
this.settings.on("change:isStatic", this.render, this);
},
createView: function() {
var margin = {top: 10, right: 10, bottom: 10, left: 10};
var availableWidth = parseInt(this.settings.get("width") || this.$el.width(), 10);
var availableHeight = parseInt(this.settings.get("height") || this.$el.height(), 10);
this.$el.html("");
var svg = d3.select(this.el)
.append("svg")
.attr("width", availableWidth)
.attr("height", availableHeight)
.attr("pointer-events", "all");
return { container: this.$el, svg: svg, margin: margin };
},
// making the data look how we want it to for updateView to do its job
formatData: function(data) {
var nodes = {};
var links = [];
data.forEach(function(link) {
var sourceName = link[0];
var targetName = link[1];
var groupName = link[2];
var newLink = {};
newLink.source = nodes[sourceName] ||
(nodes[sourceName] = {name: sourceName, group: groupName, value: 0});
newLink.target = nodes[targetName] ||
(nodes[targetName] = {name: targetName, group: groupName, value: 0});
newLink.value = +link[3];
newLink.source.value += newLink.value;
newLink.target.value += newLink.value;
links.push(newLink);
});
return {nodes: d3.values(nodes), links: links};
},
updateView: function(viz, data) {
var that = this;
var containerHeight = this.$el.height();
var containerWidth = this.$el.width();
// Clear svg
var svg = $(viz.svg[0]);
svg.empty();
svg.height(containerHeight);
svg.width(containerWidth);
// Add the graph group as a child of the main svg
var graphWidth = containerWidth - viz.margin.left - viz.margin.right;
var graphHeight = containerHeight - viz.margin.top - viz.margin.bottom;
var graph = viz.svg
.append("g")
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("transform", "translate(" + viz.margin.left + "," + viz.margin.top + ")");
// Get settings
this.charge = this.settings.get('charges');
this.gravity = this.settings.get('gravity');
this.linkDistance = this.settings.get('linkDistance');
this.zoomable = this.settings.get('panAndZoom');
this.swoop = this.settings.get('swoop');
this.isStatic = this.settings.get('isStatic');
this.isDirectional = this.settings.get('directional');
this.zoomFactor = 0.5;
this.groupNameLookup = data.groupLookup;
// Set up graph
var r = 6;
var height = graphHeight;
var width = graphWidth;
var force = d3.layout.force()
.gravity(this.gravity)
.charge(this.charge)
.linkDistance(this.linkDistance)
.size([width, height]);
this.color = d3.scale.category20();
if (this.zoomable) {
initPanZoom.call(this, viz.svg);
}
graph.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1);
graph.append("svg:defs").selectAll("marker")
.data(["arrowEnd"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("markerUnits", "userSpaceOnUse")
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = graph.selectAll("line.link")
.data(data.links)
.enter().append('path')
.attr("class", "link")
.attr("marker-end", function(d) {
if (that.isDirectional) {
return "url(#" + "arrowEnd" + ")";
}
})
.style("stroke-width", function(d) {
var num = Math.max(Math.round(Math.log(d.value)), 1);
return _.isNaN(num) ? 1 : num;
});
link
.on('click', function(d) {
that.trigger('click:link', {
source: d.source.name,
sourceGroup: d.source.group,
target: d.target.name,
targetGroup: d.target.group,
value: d.value
});
});
var node = graph.selectAll("circle.node")
.data(data.nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("r", r - 1)
.style("fill", function(d) {
return that.color(d.group);
})
.call(force.drag);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name || "";
});
//var labels = node.append("text")
// .text(function(d) { return d.name; });
node.append("title")
.text(function(d) { return d.name; });
node
.on('click', function(d) {
that.trigger('click:node', {
name: d.name,
group: d.group,
value: d.value
});
});
force.nodes(data.nodes)
.links(data.links)
.on("tick", function() {
link.attr("d", function(d) {
var diffX = d.target.x - d.source.x;
var diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
var pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
var offsetX = (diffX * (r * 2)) / pathLength;
var offsetY = (diffY * (r * 2)) / pathLength;
if (!that.swoop) {
pathLength = 0;
}
return "M" + d.source.x + "," + d.source.y + "A" + pathLength + "," + pathLength + " 0 0,1 " + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
});
node.attr("cx", function(d) {
d.x = Math.max(r, Math.min(width - r, d.x));
return d.x;
})
.attr("cy", function(d) {
d.y = Math.max(r, Math.min(height - r, d.y));
return d.y;
});
}).start();
if (this.isStatic) {
forwardAlpha(force, 0.005, 1000);
}
function forwardAlpha(layout, alpha, max) {
alpha = alpha || 0;
max = max || 1000;
var i = 0;
while (layout.alpha() > alpha && i++ < max) {
layout.tick();
}
}
// draggin'
function initPanZoom(svg) {
var that = this;
svg.on('mousedown.drag', function() {
if (that.zoomable) {
svg.classed('panCursor', true);
}
// console.log('drag start');
});
svg.on('mouseup.drag', function() {
svg.classed('panCursor', false);
// console.log('drag stop');
});
svg.call(d3.behavior.zoom().on("zoom", function() {
panZoom();
}));
}
// zoomin'
function panZoom() {
graph.attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
//TODO: This doesn't seem to be used in this file
function getSafeVal(getobj, name) {
var retVal;
if (getobj.hasOwnProperty(name) && getobj[name] !== null) {
retVal = getobj[name];
} else {
retVal = name;
}
return retVal;
}
function highlightNodes(val) {
var self = this, groupName;
if (val !== ' ' && val !== '') {
graph.selectAll('circle')
.filter(function(d, i) {
groupName = self.groupNameLookup[d.group];
if (d.source.indexOf(val) >= 0 || groupName.indexOf(val) >= 0) {
d3.select(this).classed('highlight', true);
} else {
d3.select(this).classed('highlight', false);
}
});
} else {
graph.selectAll('circle').classed('highlight', false);
}
}
}
});
return ForceDirected;
});
CSS
.splunk-toolkit-force-directed {
overflow: hidden;
font-family: arial;
}
.splunk-toolkit-force-directed circle.node {
stroke: #fff;
stroke-width: 1.5px;
}
.splunk-toolkit-force-directed .link, .splunk-toolkit-force-directed #arrowEnd {
stroke: #999;
stroke-opacity: .6;
fill: none;
}
.splunk-toolkit-force-directed #arrowEnd {
fill: #999;
}
.splunk-toolkit-force-directed circle.node {
stroke: #fff;
stroke-width: 1.5px;
}
.splunk-toolkit-force-directed circle.nodeHighlight,
.splunk-toolkit-force-directed circle.highlight {
stroke-width: 2px;
stroke: #E89595;
}
.linkHighlight {
stroke: red !important;
}
.splunk-toolkit-force-directed circle.nodeHighlight.highlight {
stroke-width: 3px;
}
.splunk-toolkit-force-directed line.link {
stroke: #999;
stroke-opacity: .6;
}
.splunk-toolkit-force-directed #chart {
width: 100%;
height: 100%;
}
.splunk-toolkit-force-directed .group-swatch {
width:20px;
height:20px;
float:left;
margin:2px;
margin-right: 10px
}
.splunk-toolkit-force-directed .group-name {
padding-top: 5px;
}
.splunk-toolkit-force-directed .panCursor {
cursor: move;
}
Got it working by obliterating tooltips. It still shows node names on mouseover, but the giant tooltip is gone. I had to remove the mouseover effect being called from the d3 library. I also changed the CSS, but I don't know if that's germane to the problem.
Javascript:
// Force Directed Graphs!
// these require an input of (at least) 3 fields in the format
// 'stats count by field1 field2 field3'
// ---- settings ----
// height, width
// panAndZoom: the ability to zoom (true, false)
// directional: true, false
// valueField: what field to count by
// charges, gravity: change the look of the graph, play around with these!
// linkDistance: the distance between each node
// ---- expected data format ----
// a splunk search like this: source=*somedata* | stats count by artist_name track_name device
// each group is an artist/song pairing
// {
// "nodes":[
// {
// "source":"Bruno Mars",
// "group":0
// },
// {
// "source":"It Will Rain",
// "group":0
// },
// {
// "source":"Cobra Starship",
// "group":1
// },
// {
// "source":"You Make Me Feel",
// "group":1
// },
// {
// "source":"Gym Class Heroes",
// "group":2
// },
// {
// "source":"Stereo Hearts",
// "group":2
// },
// ],
// "links":[
// {
// "source":0,
// "target":1,
// "value":null
// },
// {
// "source":2,
// "target":3,
// "value":null
// },
// {
// "source":4,
// "target":5,
// "value":null
// },
// ],
// - we add this part -
// "groupNames":{
// "iphone":49,
// "android":53,
// "blackberry":48,
// "ipad":52,
// "ipod":50
// },
// "groupLookup":[
// "iphone",
// "android",
// "blackberry",
// "ipad",
// "ipod"
// ]
// }
define(function(require, exports, module) {
var _ = require('underscore');
var d3 = require("../d3/d3");
var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview");
require("css!./forcedirected.css");
var ForceDirected = SimpleSplunkView.extend({
moduleId: module.id,
className: "splunk-toolkit-force-directed",
options: {
managerid: null,
data: 'preview',
panAndZoom: true,
directional: true,
valueField: 'count',
charges: -500,
gravity: 0.2,
linkDistance: 15,
swoop: false,
isStatic: true
},
output_mode: "json_rows",
initialize: function() {
SimpleSplunkView.prototype.initialize.apply(this, arguments);
// in the case that any options are changed, it will dynamically update
// without having to refresh.
this.settings.on("change:charges", this.render, this);
this.settings.on("change:gravity", this.render, this);
this.settings.on("change:linkDistance", this.render, this);
this.settings.on("change:directional", this.render, this);
this.settings.on("change:panAndZoom", this.render, this);
this.settings.on("change:swoop", this.render, this);
this.settings.on("change:isStatic", this.render, this);
},
createView: function() {
var margin = {top: 10, right: 10, bottom: 10, left: 10};
var availableWidth = parseInt(this.settings.get("width") || this.$el.width(), 10);
var availableHeight = parseInt(this.settings.get("height") || this.$el.height(), 10);
this.$el.html("");
var svg = d3.select(this.el)
.append("svg")
.attr("width", availableWidth)
.attr("height", availableHeight)
.attr("pointer-events", "all");
return { container: this.$el, svg: svg, margin: margin };
},
// making the data look how we want it to for updateView to do its job
formatData: function(data) {
var nodes = {};
var links = [];
data.forEach(function(link) {
var sourceName = link[0];
var targetName = link[1];
var groupName = link[2];
var newLink = {};
newLink.source = nodes[sourceName] ||
(nodes[sourceName] = {name: sourceName, group: groupName, value: 0});
newLink.target = nodes[targetName] ||
(nodes[targetName] = {name: targetName, group: groupName, value: 0});
newLink.value = +link[3];
newLink.source.value += newLink.value;
newLink.target.value += newLink.value;
links.push(newLink);
});
return {nodes: d3.values(nodes), links: links};
},
updateView: function(viz, data) {
var that = this;
var containerHeight = this.$el.height();
var containerWidth = this.$el.width();
// Clear svg
var svg = $(viz.svg[0]);
svg.empty();
svg.height(containerHeight);
svg.width(containerWidth);
// Add the graph group as a child of the main svg
var graphWidth = containerWidth - viz.margin.left - viz.margin.right;
var graphHeight = containerHeight - viz.margin.top - viz.margin.bottom;
var graph = viz.svg
.append("g")
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("transform", "translate(" + viz.margin.left + "," + viz.margin.top + ")");
// Get settings
this.charge = this.settings.get('charges');
this.gravity = this.settings.get('gravity');
this.linkDistance = this.settings.get('linkDistance');
this.zoomable = this.settings.get('panAndZoom');
this.swoop = this.settings.get('swoop');
this.isStatic = this.settings.get('isStatic');
this.isDirectional = this.settings.get('directional');
this.zoomFactor = 0.5;
this.groupNameLookup = data.groupLookup;
// Set up graph
var r = 6;
var height = graphHeight;
var width = graphWidth;
var force = d3.layout.force()
.gravity(this.gravity)
.charge(this.charge)
.linkDistance(this.linkDistance)
.size([width, height]);
this.color = d3.scale.category20();
if (this.zoomable) {
initPanZoom.call(this, viz.svg);
}
graph.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1);
graph.append("svg:defs").selectAll("marker")
.data(["arrowEnd"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("markerUnits", "userSpaceOnUse")
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = graph.selectAll("line.link")
.data(data.links)
.enter().append('path')
.attr("class", "link")
.attr("marker-end", function(d) {
if (that.isDirectional) {
return "url(#" + "arrowEnd" + ")";
}
})
.style("stroke-width", function(d) {
var num = Math.max(Math.round(Math.log(d.value)), 1);
return _.isNaN(num) ? 1 : num;
});
link
.on('click', function(d) {
that.trigger('click:link', {
source: d.source.name,
sourceGroup: d.source.group,
target: d.target.name,
targetGroup: d.target.group,
value: d.value
});
});
var node = graph.selectAll("circle.node")
.data(data.nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("r", r - 1)
.style("fill", function(d) {
return that.color(d.group);
})
.call(force.drag);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name || "";
});
//var labels = node.append("text")
// .text(function(d) { return d.name; });
node.append("title")
.text(function(d) { return d.name; });
node
.on('click', function(d) {
that.trigger('click:node', {
name: d.name,
group: d.group,
value: d.value
});
});
force.nodes(data.nodes)
.links(data.links)
.on("tick", function() {
link.attr("d", function(d) {
var diffX = d.target.x - d.source.x;
var diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
var pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
var offsetX = (diffX * (r * 2)) / pathLength;
var offsetY = (diffY * (r * 2)) / pathLength;
if (!that.swoop) {
pathLength = 0;
}
return "M" + d.source.x + "," + d.source.y + "A" + pathLength + "," + pathLength + " 0 0,1 " + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
});
node.attr("cx", function(d) {
d.x = Math.max(r, Math.min(width - r, d.x));
return d.x;
})
.attr("cy", function(d) {
d.y = Math.max(r, Math.min(height - r, d.y));
return d.y;
});
}).start();
if (this.isStatic) {
forwardAlpha(force, 0.005, 1000);
}
function forwardAlpha(layout, alpha, max) {
alpha = alpha || 0;
max = max || 1000;
var i = 0;
while (layout.alpha() > alpha && i++ < max) {
layout.tick();
}
}
// draggin'
function initPanZoom(svg) {
var that = this;
svg.on('mousedown.drag', function() {
if (that.zoomable) {
svg.classed('panCursor', true);
}
// console.log('drag start');
});
svg.on('mouseup.drag', function() {
svg.classed('panCursor', false);
// console.log('drag stop');
});
svg.call(d3.behavior.zoom().on("zoom", function() {
panZoom();
}));
}
// zoomin'
function panZoom() {
graph.attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
//TODO: This doesn't seem to be used in this file
function getSafeVal(getobj, name) {
var retVal;
if (getobj.hasOwnProperty(name) && getobj[name] !== null) {
retVal = getobj[name];
} else {
retVal = name;
}
return retVal;
}
function highlightNodes(val) {
var self = this, groupName;
if (val !== ' ' && val !== '') {
graph.selectAll('circle')
.filter(function(d, i) {
groupName = self.groupNameLookup[d.group];
if (d.source.indexOf(val) >= 0 || groupName.indexOf(val) >= 0) {
d3.select(this).classed('highlight', true);
} else {
d3.select(this).classed('highlight', false);
}
});
} else {
graph.selectAll('circle').classed('highlight', false);
}
}
}
});
return ForceDirected;
});
CSS
.splunk-toolkit-force-directed {
overflow: hidden;
font-family: arial;
}
.splunk-toolkit-force-directed circle.node {
stroke: #fff;
stroke-width: 1.5px;
}
.splunk-toolkit-force-directed .link, .splunk-toolkit-force-directed #arrowEnd {
stroke: #999;
stroke-opacity: .6;
fill: none;
}
.splunk-toolkit-force-directed #arrowEnd {
fill: #999;
}
.splunk-toolkit-force-directed circle.node {
stroke: #fff;
stroke-width: 1.5px;
}
.splunk-toolkit-force-directed circle.nodeHighlight,
.splunk-toolkit-force-directed circle.highlight {
stroke-width: 2px;
stroke: #E89595;
}
.linkHighlight {
stroke: red !important;
}
.splunk-toolkit-force-directed circle.nodeHighlight.highlight {
stroke-width: 3px;
}
.splunk-toolkit-force-directed line.link {
stroke: #999;
stroke-opacity: .6;
}
.splunk-toolkit-force-directed #chart {
width: 100%;
height: 100%;
}
.splunk-toolkit-force-directed .group-swatch {
width:20px;
height:20px;
float:left;
margin:2px;
margin-right: 10px
}
.splunk-toolkit-force-directed .group-name {
padding-top: 5px;
}
.splunk-toolkit-force-directed .panCursor {
cursor: move;
}