Dashboards & Visualizations

How to drill down from a D3 chart to a table using Simple XML and JavaScript?

Motivator

Hi

Does anyone know any example that shows that how to add a drilldown from a D3 chart?

I am trying to write a dashboard with a D3 chart (Bubble chart) and Splunk table.
When a user click on a label of a bubble in a Babble chart (bubbles based on ... | stats count by sourcetype), I want to have a table in a same dashboard updated based on a value of a bubble chart ( ... sourcetype=$clickedvalue_on_bubble$ | stats count by source).

I saw similar operation in WebFramework toolkit app, but I want to do it within SimpleXML + JavaScript Extension.

Any comment would be appreciated!!

0 Karma

Champion

Basically, this requires two steps: capture the click on your visualization, and on such events update a token. The difficulty of the first step depends on your visualization, as you'll have to find the right DOM element to attach your event listener to. Since many d3.js visualizations produce svg containers, finding the one which has been clicked on can be a hassle (or pretty easy).

See this working example, where you can use both a d3 wordcloud visualization and a chart to set a token used in a third panel. Simple XML:

<dashboard script="d3.js, d3.layout.cloud.js, wordcloud_for_custom.js">
  <label>custom clone</label>
  <search id="data_search">
    <query>index=_internal | top group</query>
    <earliest>-15m</earliest>
    <latest>now</latest>
  </search>
  <row>
    <panel>
      <html>
        <div id="wordcloud"/>
      </html>
    </panel>
    <panel>
      <chart>
        <search base="data_search"></search>
        <option name="charting.chart">column</option>
        <drilldown>
          <set token="group_tok">$click.value$</set>
        </drilldown>
      </chart>
    </panel>
  </row>
  <row depends="$group_tok$">
    <panel>
      <table>
        <title>Token content: $group_tok$</title>
        <search>
          <query>index=_internal group=$group_tok$ | stats count</query>
          <earliest>-15m</earliest>
          <latest>now</latest>
        </search>
        <option name="wrap">undefined</option>
        <option name="rowNumbers">undefined</option>
        <option name="drilldown">row</option>
      </table>
    </panel>
  </row>
</dashboard>

wordcloud.js:

require([
    "splunkjs/mvc",
    "splunkjs/mvc/utils",
    "splunkjs/mvc/tokenutils",
    "underscore",
    "jquery",
    "splunkjs/mvc/simplexml",
    "splunkjs/mvc/searchmanager"

    ],
    function(
        mvc,
        utils,
        TokenUtils,
        _,
        $,
        SearchManager

        ) {
            var search = splunkjs.mvc.Components.get("data_search");
            var submitted_token_model = splunkjs.mvc.Components.get("submitted");
            var myResults = search.data("results");
            var resultArray = [];
            var searchresult_tags = [];
            var searchresult_weight = [];
            var widthOfCell = 400;
            var duration = 750;
            var fontSizeRange = d3.scale.linear();
            var fill = d3.scale.category20();

            // Most of what follows is only used specifically for the wordcloud viz, you can skim it but it's not necessary
            // A group in which to present the cloud
            var cloudGroup = d3.select("#wordcloud").append("svg")
                .attr("width", widthOfCell)
                .attr("height", 300)
                .append("g")
                .attr("transform", "translate("+ widthOfCell/2 +",150)");

            // Show the user that something is happening while waiting/searching
            var notice = cloudGroup
                .append("text")
                .style("font-size", "14px")
                .style("fill", "gray")
                .attr("text-anchor", "middle")
                .attr("id", "notice")
                .text("Waiting...");

            search.on('search:progress', function() {
                notice.text("Working...");
            });

            search.on('search:done', function() {
                // Search has finished, remove notice
                cloudGroup.select("#notice").remove();
            });

            myResults.on("data", function() {
                // Fill two arrays with results (tags and weight)
                resultArray = myResults.data().rows;
                // Empty arrays (in case the new search has less results than the previous)
                searchresult_tags = [];
                searchresult_weight = [];
                for (i = 0; i < resultArray.length; i++) {
                    searchresult_tags[i] = resultArray[i][0];
                    searchresult_weight[i] = parseInt(resultArray[i][1]);
                }

                // This is used to dynamically calculate the font sizes
                fontSizeRange
                    .domain([searchresult_weight[searchresult_weight.length-1], searchresult_weight[0]])
                    .range([8, 35]);

                d3.layout.cloud()
                    .size([widthOfCell, 300])
                    .words(searchresult_tags.map(function(d, i) {
                        var result = {text: searchresult_tags[i], size: searchresult_weight[i]};
                        return result;
                    }))
                    .padding(1)
                    .rotate(function() { return ~~(Math.random() * 70) - 35 ; })
                    .font("Impact")
                    .fontSize(function(d) { return fontSizeRange(d.size); })
                    .on("end", draw)
                    .start();

                function draw(words) {
                    cloudData = cloudGroup
                        .selectAll("text")
                        .data(words);

                    // enter data
                    cloudData.enter().append("text")
                        .style("font-size", "0px")
                        .style("font-family", "Impact")
                        .style("fill", function(d, i) { return fill(i); })
                        .attr("text-anchor", "middle")
                        .attr("transform", function(d) {
                            return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
                        })
                        .text(function(d) { return d.text; })
                        .transition().duration(duration).style("font-size", function(d) { return d.size + "px"; });

                    // update data
                    cloudData
                        .transition().duration(duration).style("font-size", "0px")
                        .remove()
                        .style("font-size", "0px")
                        .attr("transform", function(d) {
                            return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
                        })
                        .text(function(d) { return d.text; })
                        .transition().duration(duration).style("font-size", function(d) { return d.size + "px"; });
                }
            });
            // This is the fun part:
            // Add click listener to all text elements in #wordcloud to set token to text
            $("#wordcloud").on("click", "text", function() {
                submitted_token_model.set("group_tok", $(this).text());
            });
        });

You'll of course need d3.js and d3.layout.cloud.js in your appserver/static directory. This example (although somewhat lengthy) is pretty easy, all I had to do was listen to the text children of my #wordcloud div.
Feel free to come back with any questions. You could also paste working code of another visualization if you manage to get something more complicated working.

0 Karma