All Apps and Add-ons

Custom Visualizations: Tooltip blocks access in Force Directed Graph. Has anyone fixed the javascript for this?

ksextonmacb
Path Finder

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.

0 Karma
1 Solution

ksextonmacb
Path Finder

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

View solution in original post

0 Karma

ksextonmacb
Path Finder

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

View solution in original post

0 Karma
Register for .conf21 Now! Go Vegas or Go Virtual!

How will you .conf21? You decide! Go in-person in Las Vegas, 10/18-10/21, or go online with .conf21 Virtual, 10/19-10/20.