3
\$\begingroup\$

I am quite new to D3 and JavaScript in general, so I'm sure there is a way to write the following in a lot less code:

function CalculatePeriodReturns(){
    periodReturns = [];
    [...]
    periodReturns.push({
        length: [...],
        mean: [...],
        pctile50: [pctile(0.25),pctile(0.75)],
        pctile90: [pctile(0.05),pctile(0.95)],
        pctile99: [pctile(0.005),pctile(0.995)],
        min: [...],
        max: [...]
    });
}

function DrawChart(){
    var margin = {top: 20, right: 100, bottom: 30, left: 100},
        width = $("#multiperiod_chart").width() - margin.left - margin.right,
        height = $("#multiperiod_chart").height() - margin.top - margin.bottom;

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

    var x = d3.scaleLinear().range([0, width]);
    var y = d3.scaleLinear().range([height, 0]);

    x.domain(d3.extent(periodReturns, d=>d.length));
    y.domain([d3.min(periodReturns, d => d.min), 
        d3.max(periodReturns, d => d.max)]); 

    var area99 = d3.area()
        .x(d=>x(d.length))
        .y0(d=>y(d.pctile99[0]))      
        .y1(d=>y(d.pctile99[1]))    

    svg.append("path")
        .attr("class", "area")
        .attr("id", "area99")
        .attr("fill", "#565A68")
        .attr("d", area99);

    var area90 = d3.area()
        .x(d=>x(d.length))
        .y0(d=>y(d.pctile90[0]))      
        .y1(d=>y(d.pctile90[1]))    

    svg.append("path")
        .attr("class", "area")
        .attr("id", "area90")
        .attr("fill", "#7B7F8B")
        .attr("d", area90);

    var area50 = d3.area()
        .x(d=>x(d.length))
        .y0(d=>y(d.pctile50[0]))      
        .y1(d=>y(d.pctile50[1]))    

    svg.append("path")
        .attr("class", "area")
        .attr("id", "area50")
        .attr("fill", "#A1A3AA")
        .attr("d", area50);

    [...]

}

For a start, is there a way to create an accessor area(y0,y1) that takes the two d.pctileXX properties as attributes or similar? Maybe I can combine the three svg.append's as well?

\$\endgroup\$
3
\$\begingroup\$

You can simplify your area generators, as well as your code to append the paths, using an each to append the paths or an object to define the x and y methods in the area generator.

However, there is an simpler alternative: you are facing all those difficulties because of the structure of your data. If you change your data's structure for a more adequate one you can create a code which is way more idiomatic, therefore easier to read and, the most important, easier to maintain.

Right now, this is your data structure:

var periodReturns = [{
    pectile99: [x, y],
    pectile90: [x, y],
    pectile50: [x, y]
}, {
    pectile99: [x, y],
    pectile90: [x, y],
    pectile50: [x, y]
}, {
    etc...
}];

You will bend over backwards to create a multiarea chart with a data structure like this, because:

  1. The data() methods accepts a data array in which each object is a datum;
  2. You cannot easily get the objects' keys;

So, let's change that data structure. We'll created a nested data, with a key/value pair for the name, conveniently called name, and a key/value pair (containing an array) for the points, conveniently called values:

var data = [{
  pectile99: [0, 10],
  pectile90: [0, 40],
  pectile50: [0, 100]
}, {
  pectile99: [100, 40],
  pectile90: [100, 140],
  pectile50: [100, 30]
}, {
  pectile99: [200, 110],
  pectile90: [200, 10],
  pectile50: [200, 100]
}, {
  pectile99: [250, 90],
  pectile90: [250, 70],
  pectile50: [250, 140]
}, {
  pectile99: [300, 10],
  pectile90: [300, 30],
  pectile50: [300, 150]
}];

var nestedData = Object.keys(data[0]).map(function(d) {
  var obj = {
    name: d,
    values: []
  };
  for (var i = 0; i < data.length; i++) {
    obj.values.push(data[i][d])
  };
  return obj;
});

console.log(nestedData)

Now, since all the points can be accessed using the same property name, we can easily write just one area generator. Also, since each object in nestedData corresponds to just one area, we can write a single enter selection!

var areaGenerator = d3.area()
    .x(function(d) {
        return d[0]
    })
    .y1(function(d) {
        return d[1]
    })
    .y0(150);

var areas = svg.selectAll(null)
    .data(nestedData)
    .enter()
    .append("path")
    .attr("fill", function(d, i) {
        return color(i)
    })
    .attr("opacity", 0.5)
    .attr("d", function(d) {
        return areaGenerator(d.values);
    })

Here is the demo:

var data = [{
  pectile99: [0, 10],
  pectile90: [0, 40],
  pectile50: [0, 100]
}, {
  pectile99: [100, 40],
  pectile90: [100, 140],
  pectile50: [100, 30]
}, {
  pectile99: [200, 110],
  pectile90: [200, 10],
  pectile50: [200, 100]
}, {
  pectile99: [250, 90],
  pectile90: [250, 70],
  pectile50: [250, 140]
}, {
  pectile99: [300, 10],
  pectile90: [300, 30],
  pectile50: [300, 150]
}];

var svg = d3.select("svg");

var color = d3.scaleOrdinal(d3.schemeCategory10)

var prop;

var nestedData = Object.keys(data[0]).map(function(d) {
  var obj = {
    name: d,
    values: []
  };
  for (var i = 0; i < data.length; i++) {
    obj.values.push(data[i][d])
  };
  return obj;
});

var areaGenerator = d3.area()
  .x(function(d) {
    return d[0]
  })
  .y1(function(d) {
    return d[1]
  })
  .y0(150);

var areas = svg.selectAll(null)
  .data(nestedData)
  .enter()
  .append("path")
  .attr("fill", function(d, i) {
    return color(i)
  })
  .attr("opacity", 0.5)
  .attr("d", function(d) {
    return areaGenerator(d.values);
  })
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

\$\endgroup\$
  • \$\begingroup\$ Thanks a lot! That doesn't quite solve it though - mind we need more than just the value pair to create an area chart, we also need "length": .x(d=>x(d.length)).y0(d=>y(d.pctile99[0])).y1(d=>y(d.pctile99[1])) We're more than free to change how the data gets populated in the first place by the way - my periodReturns.push({}) approach was just my own way of doing this without putting a lot of thought into this, and frankly I don't know any better! \$\endgroup\$ – Oli Aug 30 '17 at 15:04
  • \$\begingroup\$ @Oli The principle remains, just push any value you want inside the inner arrays. \$\endgroup\$ – Gerardo Furtado Aug 31 '17 at 3:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.