Monday, March 19, 2018

Take your OBIEE graphs and charts to new level with D3 (Part 2.1)

Let's upgrade our collapsible Tree a little to include some values for each node. Something like this ...
At first we would need data values added to the analysis. That means we need to add data values to each result columns of  "union all" section. (see Part 2)
Once we do that, rest of the steps will be very similar...



Prefix:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    
.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

div.tooltip {
    position: absolute;
    text-align: center;
    width: 80px;
    height: 28px;
    padding: 2px;
    font: 12px sans-serif;
    background: lightsteelblue;
    border: 0px;
    border-radius: 8px;
    pointer-events: none;
}
</style>



<svg width="1200" height="1500"></svg>
<script src="/analyticsRes/libraries/d3.v3.min.js"></script>
<script>



var data=[];

Narrative:

data.push({name:"@2",parent:"@1", Expense:"@3"}); 

Postfix:



var dataMap = data.reduce(function(map, node) {
 map[node.name] = node;
 return map;
}, {});

var treeData = [];
data.forEach(function(node) {
 // add to parent
 var parent = dataMap[node.parent];
 if (parent) {
  // create child array if it doesn't exist
  (parent.children || (parent.children = []))
   // add node to child array
   .push(node);
 } else {
  // parent is null or missing
  treeData.push(node);
 }
});

// Define the div for the tooltip
var div = d3.select("body").append("div")
    .attr("class", "tooltip")
    .style("opacity", 0);

var margin = {top: 20, right: 300, bottom: 20, left: 300},
    width = 1500 - margin.right - margin.left,
    height = 900 - margin.top - margin.bottom;

var i = 0,
    duration = 750,
    root;

 
var tree = d3.layout.tree()
    .size([height, width]);

var diagonal = d3.svg.diagonal()
    .projection(function(d) { return [d.y, d.x]; });

var svg = d3.select("svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  root = treeData[0];
  root.x0 = height / 2;
  root.y0 = 0;

  function collapse(d) {
    if (d.children) {
      d._children = d.children;
      d._children.forEach(collapse);
      d.children = null;
    }
  }

  root.children.forEach(collapse);
  update(root);

d3.select(self.frameElement).style("height", "800px");

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = d.depth * 180; });

  // Update the nodes…
  var node = svg.selectAll("g.node")
      .data(nodes, function(d) { return d.id || (d.id = ++i); });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
      .on("click", click).on("mouseover", function(d) {
             div.transition()
                .duration(200)
                .style("opacity", .9);
             div.html("Expense $"+d.Expense + "<br/>"  )
                .style("left", (d3.event.pageX) + "px")
                .style("top", (d3.event.pageY - 28) + "px");
            })
  .on("mouseout", function(d) {
            div.transition()
                .duration(500)
                .style("opacity", 0);
});

  nodeEnter.append("circle")
      .attr("r", 1e-6)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  nodeEnter.append("text")
      .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
      .attr("dy", ".35em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.name; })
      .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

  nodeUpdate.select("circle")
      .attr("r", 4.5)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  nodeUpdate.select("text")
      .style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.
  var nodeExit = node.exit().transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
      .remove();

  nodeExit.select("circle")
      .attr("r", 1e-6);

  nodeExit.select("text")
      .style("fill-opacity", 1e-6);

  // Update the links…
  var link = svg.selectAll("path.link")
      .data(links, function(d) { return d.target.id; });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
      .attr("class", "link")
      .attr("d", function(d) {
        var o = {x: source.x0, y: source.y0};
        return diagonal({source: o, target: o});
      });

  // Transition links to their new position.
  link.transition()
      .duration(duration)
      .attr("d", diagonal);

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
      .duration(duration)
      .attr("d", function(d) {
        var o = {x: source.x, y: source.y};
        return diagonal({source: o, target: o});
      })
      .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}
</script>

Thursday, March 15, 2018

Take your OBIEE graphs and charts to new level with D3 (Part 2)

Collapsible Tree


Please read my first blog here for set up needed to start your OBIEE D3 visualization.

In this blog you will see,  how you can have interactive Essbase hierarchy(or any other hierarchy) displayed through OBIEE like above.

We will do it with hierarchical data added as parent / child in analysis and then D3 will do the rest. We will use "union all" in the analysis to achieve this.

The analysis will look like this ...
sort order column will make sure data is sorted by generation. We will put simple 'A', 'B','C' to identify each generation.

Generation 1...

Generation 2 ...


 Generation 3...

And so on ...

We also need to make sure that Leaf nodes are not present in parent column.... to do that we will add filter to each generation query. Assuming at least root node exists for the tree to built. 

So Filter will look like ....  

Gen 1 filter
none

Gen 2 Filter

Gen 3 Filter


And so on. ....

The above steps will provide you data in the narrative view that you can use in your D3 code.

Lets look at the D3 code... 

Prefix

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    
.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

</style>



<svg width="1200" height="1500"></svg>
<script src="/analyticsRes/libraries/d3.v3.min.js"></script>
<script>



var data=[];


Narrative
data.push({name:"@2",parent:"@1"});


Postfix


var dataMap = data.reduce(function(map, node) {
 map[node.name] = node;
 return map;
}, {});

var treeData = [];
data.forEach(function(node) {
 // add to parent
 var parent = dataMap[node.parent];
 if (parent) {
  // create child array if it doesn't exist
  (parent.children || (parent.children = []))
   // add node to child array
   .push(node);
 } else {
  // parent is null or missing
  treeData.push(node);
 }
});


var margin = {top: 20, right: 300, bottom: 20, left: 300},
    width = 1500 - margin.right - margin.left,
    height = 900 - margin.top - margin.bottom;

var i = 0,
    duration = 750,
    root;

var tree = d3.layout.tree()
    .size([height, width]);

var diagonal = d3.svg.diagonal()
    .projection(function(d) { return [d.y, d.x]; });

var svg = d3.select("svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  root = treeData[0];
  root.x0 = height / 2;
  root.y0 = 0;

  function collapse(d) {
    if (d.children) {
      d._children = d.children;
      d._children.forEach(collapse);
      d.children = null;
    }
  }

  root.children.forEach(collapse);
  update(root);

d3.select(self.frameElement).style("height", "800px");

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = d.depth * 180; });

  // Update the nodes…
  var node = svg.selectAll("g.node")
      .data(nodes, function(d) { return d.id || (d.id = ++i); });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
      .on("click", click);

  nodeEnter.append("circle")
      .attr("r", 1e-6)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  nodeEnter.append("text")
      .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
      .attr("dy", ".35em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.name; })
      .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

  nodeUpdate.select("circle")
      .attr("r", 4.5)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  nodeUpdate.select("text")
      .style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.
  var nodeExit = node.exit().transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
      .remove();

  nodeExit.select("circle")
      .attr("r", 1e-6);

  nodeExit.select("text")
      .style("fill-opacity", 1e-6);

  // Update the links…
  var link = svg.selectAll("path.link")
      .data(links, function(d) { return d.target.id; });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
      .attr("class", "link")
      .attr("d", function(d) {
        var o = {x: source.x0, y: source.y0};
        return diagonal({source: o, target: o});
      });

  // Transition links to their new position.
  link.transition()
      .duration(duration)
      .attr("d", diagonal);

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
      .duration(duration)
      .attr("d", function(d) {
        var o = {x: source.x, y: source.y};
        return diagonal({source: o, target: o});
      })
      .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}
</script>



Thursday, March 8, 2018

Take your OBIEE graphs and charts to new level with D3 (Part 1)



Sortable Bar Chart using d3-tip to add tooltips


It really started with a user asking me whether OBIEE has donut chart. I said “no”, not with the out of the box OBIEE. I know that OBIEE has great ability to integrate external applications through the use of java script libraries but I never had time to explore that. Once I started to dig deeper, I got fascinated by the possibility.  Stunning visualization examples of D3 (Data-Driven Documents) blew my mind.

     If you never been to D3 example page time to pay homage before you read further. https://github.com/d3/d3/wiki/Gallery



This blog is not about how you can have your first D3 + OBIEE chart. Rather, I would like you to google D3 + OBIEE. Read very informative blogs by Rittman Mead, Red Pill Analytics etc.  
Here in this blog I will share the code that I used in OBIEE narrative view to achieve what you see in the screen shot.
This is a combination of the following 2 D3 codes.


Prerequisite

1.       Set your analyticsRes folder for your D3 library. If you don’t know how to set up a custom folder in OBIEE, google “Deploy a custom folder in OBIEE”.
2.       Download libraries and keep them in analyticsRes folder.
d3.v3.min.js
d3.tip.v0.6.3.js

3.       Once you set that up properly, you should be able to call D3 libraries like below from your browser.
http://YourObieeServer:9502/analyticsRes/libraries/d3.v3.min.js

Prefix

<!DOCTYPE html>

<meta charset="utf-8">
<style>
.axis text {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.bar {
  fill: orange;
  fill-opacity: .6;
}

.bar:hover {
  fill: orangered ;
}

.x.axis path {
  display: none;
}
.d3-tip {
  line-height: 1;
  font-weight: bold;
  padding: 12px;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  border-radius: 2px;
}

/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
  box-sizing: border-box;
  display: inline;
  font-size: 10px;
  width: 100%;
  line-height: 1;
  color: rgba(0, 0, 0, 0.8);
  content: "\25BC";
  position: absolute;
  text-align: center;
}

/* Style northward tooltips differently */
.d3-tip.n:after {
  margin: -1px 0 0 0;
  top: 100%;
  left: 0;
}
</style>
<svg width="960" height="1060"></svg>
<script src="/analyticsRes/libraries/d3.v3.min.js"></script>
<script src="/analyticsRes/libraries/d3.tip.v0.6.3.js"></script>

<script>
var data=[];


Narrative

data.push({letter:"@1",frequency:"@2"});


Postfix



var margin = {top: 20, right: 20, bottom: 90, left: 40},
    width = 640 ;
    height = 290 ;
var formatPercent = d3.format(".2s");

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1, 1);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .tickFormat(formatPercent);

var svg = d3.select("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var tip = d3.tip()
  .attr('class', 'd3-tip')
  .offset([-10, 0])
  .html(function(d) {
    return "<strong>Vendor: " +d.letter+"</strong> <span> <p> Expense:$"   +d.frequency +"</p></span>";
  });


svg.call(tip);
  data.forEach(function(d) {
    d.frequency = +d.frequency;
  });

  x.domain(data.map(function(d) { return d.letter; }));
  y.domain([0, d3.max(data, function(d) { return d.frequency; })]);

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis).selectAll("text").style("text-anchor", "end")
                        .style("font-style","italic")
            .attr("dx", ".5em")
            .attr("dy", ".1em")
            .attr("transform", function(d) {
                  return "rotate(-25)"
                });
     

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 8)
      .attr("dy", ".8em")
      .style("text-anchor", "middle")
      .text("Expense");

  svg.selectAll(".bar")
      .data(data)
    .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.letter); })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.frequency); })
      .attr("height", function(d) { return height - y(d.frequency); })     
      .on('mouseover', tip.show)
      .on('mouseout', tip.hide);


   d3.select("#obiee").append("input").attr("checked", true)
    .attr("type", "checkbox").on("change", change);


  var sortTimeout = setTimeout(function() {
    d3.select("#obiee").attr("type", "checkbox").each(change);
  }, 2000);

  function change() {
    clearTimeout(sortTimeout);

    // Copy-on-write since tweens are evaluated after a delay.
    var x0 = x.domain(data.sort(this.checked
        ? function(a, b) { return b.frequency - a.frequency; }
        : function(a, b) { return d3.ascending(a.letter, b.letter); })
        .map(function(d) { return d.letter; }))
        .copy();

    svg.selectAll(".bar")
        .sort(function(a, b) { return x0(a.letter) - x0(b.letter); });

    var transition = svg.transition().duration(750),
        delay = function(d, i) { return i * 50; };

    transition.selectAll(".bar")
        .delay(delay)
        .attr("x", function(d) { return x0(d.letter); });

    transition.select(".x.axis")
        .call(xAxis).selectAll("text").style("text-anchor", "end")
            .attr("dx", ".5em")
            .attr("dy", ".1em")
            .attr("transform", function(d) {
                  return "rotate(-25)"
                }).delay(delay);
  }
;



</script>

<div id="obiee">

</div>


Read Part 2 on D3 Collapsible Tree