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;
}
0 Karma
Get Updates on the Splunk Community!

Index This | I am a number, but when you add ‘G’ to me, I go away. What number am I?

March 2024 Edition Hayyy Splunk Education Enthusiasts and the Eternally Curious!  We’re back with another ...

What’s New in Splunk App for PCI Compliance 5.3.1?

The Splunk App for PCI Compliance allows customers to extend the power of their existing Splunk solution with ...

Extending Observability Content to Splunk Cloud

Register to join us !   In this Extending Observability Content to Splunk Cloud Tech Talk, you'll see how to ...