2

I just started learning javascript and d3.js by taking a couple of lynda.com courses. My objective is to create a function that takes an array of numbers and a cutoff and produces a plot like this one:

density

I was able to write javascript code that generates this:

enter image description here

Alas, I'm having troubles figuring out a way to tell d3.js that the area to the left of -opts.threshold should be read, the area in between -opts.threshold and opts.threshold blue, and the rest green.

This is my javascript code:

HTMLWidgets.widget({

  name: 'IMposterior',

  type: 'output',

  factory: function(el, width, height) {

    // TODO: define shared variables for this instance

    return {

      renderValue: function(opts) {

        console.log("MME: ", opts.MME);
        console.log("threshold: ", opts.threshold);
        console.log("prob: ", opts.prob);
        console.log("colors: ", opts.colors);


        var margin = {left:50,right:50,top:40,bottom:0};

        var xMax = opts.x.reduce(function(a, b) {
          return Math.max(a, b);

        });
        var yMax = opts.y.reduce(function(a, b) {
          return Math.max(a, b);

        });
        var xMin = opts.x.reduce(function(a, b) {
          return Math.min(a, b);

        });
        var yMin = opts.y.reduce(function(a, b) {
          return Math.min(a, b);

        });


        var y = d3.scaleLinear()
                    .domain([0,yMax])
                    .range([height,0]);

        var x = d3.scaleLinear()
                    .domain([xMin,xMax])
                    .range([0,width]);


        var yAxis = d3.axisLeft(y);
        var xAxis = d3.axisBottom(x);


        var area = d3.area()
                         .x(function(d,i){ return x(opts.x[i]) ;})
                         .y0(height)
                         .y1(function(d){ return y(d); });

        var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
        var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");


        chartGroup.append("path")
             .attr("d", area(opts.y));


        chartGroup.append("g")
            .attr("class","axis x")
            .attr("transform","translate(0,"+height+")")
            .call(xAxis);


      },

      resize: function(width, height) {

        // TODO: code to re-render the widget with a new size

      }

    };
  }
});

In case this is helpful, I saved all my code on a public github repo.

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
Ignacio
  • 7,646
  • 16
  • 60
  • 113

2 Answers2

1

There are two proposed solutions in this answer, using gradients or using multiple areas. I will propose an alternate solution: Use the area as a clip path for three rectangles that together cover the entire plot area.

Make rectangles by creating a data array that holds the left and right edges of each rectangle. Rectangle height and y attributes can be set to svg height and zero respectively when appending rectangles, and therefore do not need to be included in the array.

The first rectangle will have a left edge at xScale.range()[0], the last rectangle will have an right edge of xScale.range()[1]. Intermediate coordinates can be placed with xScale(1), xScale(-1) etc.

Such an array might look like (using your proposed configuration and x scale name):

var rects = [ 
              [x.range()[0],x(-1)],
              [x(-1),x(1)],
              [x(1),x.range()[1]]
            ]

Then place them:

.enter()
  .append("rect")
  .attr("x", function(d) { return d[0]; })
  .attr("width", function(d) { return d[1] - d[0]; })
  .attr("y", 0)
  .attr("height",height)

Don't forget to set a clip-path attribute for the rectangles: .attr("clip-path","url(#areaID)"), and to set fill to three different colors.

Now all you have to do is set your area's fill and stroke to none, and append your area to a clip path with the specified id:

svg.append("clipPath)
.attr("id","area")
.append("path")
.attr( // area attributes
...

Here's the concept in action (albeit using v3, which shouldn't affect the rectangles or text paths.

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
0

Thanks to @andrew-reid suggestion, I was able to implement the solution that uses multiple areas.

enter image description here

HTMLWidgets.widget({

  name: 'IMposterior',

  type: 'output',

  factory: function(el, width, height) {

    // TODO: define shared variables for this instance

    return {

      renderValue: function(opts) {

        console.log("MME: ", opts.MME);
        console.log("threshold: ", opts.threshold);
        console.log("prob: ", opts.prob);
        console.log("colors: ", opts.colors);
        console.log("data: ", opts.data);


        var margin = {left:50,right:50,top:40,bottom:0};

        xMax = d3.max(opts.data, function(d) { return d.x ; });
        yMax = d3.max(opts.data, function(d) { return d.y ; });
        xMin = d3.min(opts.data, function(d) { return d.x ; });
        yMin = d3.min(opts.data, function(d) { return d.y ; });

        var y = d3.scaleLinear()
                    .domain([0,yMax])
                    .range([height,0]);

        var x = d3.scaleLinear()
                    .domain([xMin,xMax])
                    .range([0,width]);


        var yAxis = d3.axisLeft(y);
        var xAxis = d3.axisBottom(x);


        var area = d3.area()
                         .x(function(d){ return x(d.x) ;})
                         .y0(height)
                         .y1(function(d){ return y(d.y); });

        var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
        var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");


        chartGroup.append("path")
             .attr("d", area(opts.data.filter(function(d){  return d.x< -opts.MME ;})))
             .style("fill", opts.colors[0]);

        chartGroup.append("path")
             .attr("d", area(opts.data.filter(function(d){  return d.x > opts.MME ;})))
             .style("fill", opts.colors[2]);

        if(opts.MME !==0){
          chartGroup.append("path")
             .attr("d", area(opts.data.filter(function(d){  return (d.x < opts.MME & d.x > -opts.MME) ;})))
             .style("fill", opts.colors[1]);
        }


        chartGroup.append("g")
            .attr("class","axis x")
            .attr("transform","translate(0,"+height+")")
            .call(xAxis);


      },

      resize: function(width, height) {

        // TODO: code to re-render the widget with a new size

      }

    };
  }
});
Ignacio
  • 7,646
  • 16
  • 60
  • 113