Stacked Area Chart in nvd3js - X Axis overflow
Asked Answered
C

1

7

I am trying to implement a 'Stacked Area Chart' with d3js and nvd3.js similar to this example. Additionally, I'd like to use a context brush like this one to select a date range, which effects the Stacked Area Chart. Actually, this is already working but somehow it draws some lines on top of the Y-Axis as soon as the selected date range does not contain the first date. Just have a look on the following picture: This is the bug

Here is my code:

Stacked Area Chart

var margin = {
    top : 10,
    right : 20,
    bottom : 100,
    left : 20
}, width = 960, height = 300;

var svg_stack = d3.select("#stack").append("svg").attr("width", width + margin.left + margin.right).attr("height", (height + margin.top + margin.bottom));
function initStackChart() {
    nv.addGraph(function() {
        var chart = nv.models.stackedAreaChart().x(function(d) {
            return Date.parse(new Date(d[0]))
        }).y(function(d) {
            return d[1]
        }).clipEdge(false);

        chart.xAxis.tickFormat(function(d) {
            return d3.time.format('%x')(new Date(d))
        });

        chart.yAxis.tickFormat(d3.format(',.2f'));

        if (!!time_range) {
            chart.xDomain([time_range[0], time_range[1]]);
        }

        d3.select('#stack svg').datum(temp_data).transition().duration(100).call(chart);

        nv.utils.windowResize(chart.update);
        return chart;
    });
}

Brush

var margin = {top: 10, right: 20, bottom: 0, left: 20},
  width = 960,
  height = 50;

var contextHeight = 50;
  contextWidth = width;

var parseDate = d3.time.format("%Y-%m-%d").parse;

var x = d3.time.scale().range([0, width]),
  y = d3.scale.linear().range([contextHeight, 0]);

var xAxis =    d3.svg.axis().scale(x).tickSize(contextHeight).tickPadding(-10).orient("bottom");

var brush = d3.svg.brush()
.x(x)
.on("brush", brushed);

var area2 = d3.svg.area()
.interpolate("monotone")
.x(function(d) { return x(d.time); })
.y0(contextHeight)
.y1(0);

var svg_brush = d3.select("#brush").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

svg_brush.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);

var context = svg_brush.append("g").attr("class","context")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

function initBrush(data)
{
  x.domain(d3.extent(data.map(function(d) { return d.time; })));
  context.append("g")
    .attr("class", "x axis top")
    .attr("transform", "translate(0,0)")
    .call(xAxis);

  context.append("g")
    .attr("class", "x brush")
    .call(brush)
    .selectAll("rect")
    .attr("y", 0)
    .attr("height", contextHeight);
};

function brushed() {
  var b = brush.empty() ? x.domain() : brush.extent();
  console.log(b);
  time_range=b;
  initStackChart();
}

Data

var temp_data = [
{
key: "Node0",
values:
  [      
    [  1364795940000, 10 ],
    [  1365020480000, 30 ],
    [  1365630480000, 30 ],
    [  1366000480012, 30 ],
    [  1366012740000, 0  ]  
  ]
},
{
key: "Node1",
values:
  [      
    [  1364795940000, 10 ],
    [  1365020480000, 20 ],
    [  1365630480000, 34 ],
    [  1366000480012, 82 ],
    [  1366012740000, 0  ]  
  ]
},
{
key: "Node2",
values:
  [      
    [  1364795940000, 20 ],
    [  1365020480000, 10 ],
    [  1365630480000, 0 ],
    [  1366000480012, 100 ],
    [  1366012740000, 80  ]   
  ]
},
{
key: "Node3",
values:
  [      
    [  1364795940000, 10 ],
    [  1365020480000, 60 ],
    [  1365630480000, 10 ],
    [  1366000480012, 10 ],
    [  1366012740000, 10  ]   
  ]
},
{
key: "Node4",
values:
  [      
    [  1364795940000, 16 ],
    [  1365020480000, 32 ],
    [  1365630480000, 10 ],
    [  1366000480012, 90 ],
    [  1366012740000, 10  ]  
  ]
},
{
key: "Node5",
values:
  [      
    [  1364795940000, 10 ],
    [  1365020480000, 50 ],
    [  1365630480000, 10 ],
    [  1366000480012, 20 ],
    [  1366012740000, 110  ]  
  ]
},
{
key: "Node6",
values:
  [      
    [  1364795940000, 19 ],
    [  1365020480000, 55 ],
    [  1365630480000, 32 ],
    [  1366000480012, 12 ],
    [  1366012740000, 12  ]  
  ]
},
{
key: "Node7",
values:
  [      
    [  1364795940000, 0 ],
    [  1365020480000, 20 ],
    [  1365630480000, 40 ],
    [  1366000480012, 30 ],
    [  1366012740000, 20  ]  
  ]
},
{
key: "Node8",
values:
  [      
    [  1364795940000, 12 ],
    [  1365020480000, 31 ],
    [  1365630480000, 40 ],
    [  1366000480012, 20 ],
    [  1366012740000, 15  ]  
  ]
},
{ 
key: "Node9",
values:
  [      
    [  1364795940000, 10 ],
    [  1365020480000, 35 ],
    [  1365630480000, 50 ],
    [  1366000480012, 30 ],
    [  1366012740000, 90 ]  
  ]
}
]

Thank you.

Chapnick answered 17/1, 2014 at 16:4 Comment(3)
It sounds to me like you would want to filter tempData which you're passing in to draw the graph.Takakotakakura
I am not really sure by myself. What is the correct way? I thought of "zooming" into the chart by defining a range on the brush. Is it usual to filter the data instead?Chapnick
Zooming the chart should work, but somehow you are zooming the clipping path as well. You might want to look at NVD3's line chart with viewfinder example to see how they do it.Lianneliao
L
5

Change .clipEdge(false); to .clipEdge(true); in your chart settings.

Edit

Okay, I've managed to recreate your problem on the NVD3 live code site with the following code (data and markup the same as their stacked graph example):

nv.addGraph(function() {
  var chart = nv.models.stackedAreaChart()
                .x(function(d) { return d[0] })
                .y(function(d) { return d[1] })
                .clipEdge(true);

  var chart2 = nv.models.stackedAreaChart()
                .x(function(d) { return d[0] })
                .y(function(d) { return d[1] })
                .xDomain([1096516800000, 1270008000000])
                .clipEdge(true);

  chart.xAxis
      .showMaxMin(false)
      .tickFormat(function(d) { return d3.time.format('%x')(new Date(d)) });    
  chart.yAxis
      .tickFormat(d3.format(',.2f'));

  chart2.xAxis
      .showMaxMin(false)
      .tickFormat(function(d) { return d3.time.format('%x')(new Date(d)) });    
  chart2.yAxis
      .tickFormat(d3.format(',.2f'));

  d3.select('#chart svg')
    .datum(data)
      .transition().duration(500).call(chart)
      .transition().delay(3000).duration(500)
        .call(chart2);

  nv.utils.windowResize(chart.update);

  return chart2;
});

Which is basically what you are doing -- creating a completely new chart function, and calling it on the same container. The chart function mostly selects all the same objects, and changes their attributes -- resulting in the smooth transition. But, the random id code it gives to the <clipPath> element (to ensure that each element has a unique id) no longer matches up with the one it uses as the "clip-path" attribute. You could call this a bug in the NVD3 code, but it is also partly because you are using the function in unexpected ways.

In contrast, if I use this code:

nv.addGraph(function() {
  var chart = nv.models.stackedAreaChart()
                .x(function(d) { return d[0] })
                .y(function(d) { return d[1] })
                .clipEdge(true);

  chart.xAxis
      .showMaxMin(false)
      .tickFormat(function(d) { return d3.time.format('%x')(new Date(d)) });

  chart.yAxis
      .tickFormat(d3.format(',.2f'));

  var svg = d3.select('#chart svg')
    .datum(data)
      .transition().duration(500).call(chart);

  nv.utils.windowResize(chart.update);

  var change = window.setTimeout(function(){
        chart.xDomain([1096516800000, 1270008000000]);
        chart.update();
  }, 3000);

  return chart;
});

The clipping paths still work nicely. Notice the difference? Instead of creating and calling an entire new chart function, I have just updated the chart function with the new domain, and called the function's update() method. Try re-arranging your brushing function to do the update that way, and not only should you fix your clipping path problem, but your code should be faster as well.

Edit 2

So how to implement this with your original code?

First, you need to save the chart-function object created within nv.addGraph() into a variable that can be accessed by your brushed() function.

Then, in your brushed() function, you modify your saved chart-function to apply the new x-domain, and then call the function object's update method.

var margin = {
    top : 10,
    right : 20,
    bottom : 100,
    left : 20
}, width = 960, height = 300;

var chart; // NEW! declare a variable that can be accessed by both
             // initialization and update functions

var svg_stack = d3.select("#stack")
                  .append("svg")
                  .attr("width", width + margin.left + margin.right)
                  .attr("height", (height + margin.top + margin.bottom));
function initStackChart() {
    nv.addGraph(function() {
        chart = nv.models.stackedAreaChart() 
                   // NEW! no "var" statement!
                   // this gets assigned to the chart variable declared above 

          /* rest of chart initialization code, the same as before */

   });
}

/* All the initialization code for the timeline brushing goes here, until: */

function brushed() {
  var b = brush.empty() ? x.domain() : brush.extent();
  console.log(b);
  time_range=b;

  chart.xDomain(b);  //modify the saved chart object
  chart.update();    //update the chart using the saved function
}
Lianneliao answered 17/1, 2014 at 16:6 Comment(6)
Actually, this is what i tried before I changed it to false. It produces the same failure. However, thanks for your post.Chapnick
@Chapnick I can't figure out why it wouldn't work, unless the problem is that you are never deleting the old graph before drawing the new one. Can you post what your DOM looks like after an update? Not every node, of course, just the major <g> elements, including their class, clip-path and translate attributes.Lianneliao
As far as I know I do not delete any old graphs. I made two screenshots of my DOM elements. The first one is before any modifcations: s14.directupload.net/images/140117/hvejlfpz.png And the second contains the bug: s14.directupload.net/images/140117/5urcym8g.png. Is there any other way to copy DOM elements as ascii strings? Thank you.Chapnick
@Chapnick Thanks for the images, although I should have mentioned that I needed to see the clipPath elements as well. You can usually right-click on the DOM inspector to copy elements as HTML text, but then you'd have to manually delete the inner content. Either way, I was able to recreate the problem and figure out what was going on -- see the edit to the answer.Lianneliao
thank you very much for your efforts! However, I am still not completely sure how to pass over the new time_range. Do I have to call it like change(time_range)? This is not working, since change is mentioned inside of the nv.addgraph function. That's why I get an "Uncaught Reference Error: change is not defined". Am I missing something?Chapnick
@caiuspb. Sorry I confused things. It's good code format to save the results of a setTimeout() call to a variable so you can cancel the timer, but that's all change is -- a reference to the timer I used to delay the update so you could see it happen. The important part is to save your chart function when you initialize it -- currently it is returned from the nv.addGraph() but then discarded. My comment's too long so I'll add another update to clarify...Lianneliao

© 2022 - 2024 — McMap. All rights reserved.