d3.js brush with multiline chart

2.8k views Asked by At

I'm making a multi-line chart and using a brush to select time periods. It's broadly based on Mike Bostock's example at http://bl.ocks.org/mbostock/1667367

My chart is at http://lowercasen.com/dev/d3/general/piezobrush.html

My problem is in selecting the multiple lines in my 'focus' area to apply the brush to. I've nested the data based on a key, so the data is within a function. Because the function that calls my brush is outside that function, it can't access the data and I'm getting a TypeError: undefined is not an object (evaluating 'data.length')

Here's the code that nests the data:

     dataNest.forEach(function(d, i) {

       focus.append("path")
           .attr("class", "line")
                 .attr("id", d.key.replace(/\s+/g, ''))  //the replace stuff is getting rid of spaces
                 .attr("d", levelFocus(d.values)); 

       context.append("path")
            .attr("class", "line")
                 .attr("id", d.key.replace(/\s+/g, ''))  //the replace stuff is getting rid of spaces
                 .attr("d", levelContext(d.values)); 

and at the bottom I have the function for the brush:

     function brushed() {
       xFocus.domain(brush.empty() ? xContext.domain() : brush.extent());
       focus.selectAll(".line").attr("d", levelFocus(d.values));
       focus.select(".x.axis").call(xAxisFocus);
     }

It works fine for the x axis (if I comment out the line where I'm trying to select the lines) but I don't know how to select the lines correctly.

Apologies for any garbled syntax or confusing language, my coding skills are basic at best.

Any help is greatly appreciated, I've searched for hours for a solution.

Here's the full code as requested by Lars

    <!DOCTYPE html>
    <html lang="en">
    <head>
            <meta charset="utf-8">
            <title>Multiline with brush</title>
    <script src="http://d3js.org/d3.v3.js"></script>
    <script src="d3/tooltip.js"></script>
        <link href="styles/evidentlySoCharts.css" rel="stylesheet">
        <meta name="viewport" content="initial-scale=1">   

    <style>

    svg {
      font: 10px sans-serif;
    }

    path { 
        stroke-width: 1;
        fill: none;
    }

    #Stream1, #Nebo1D {
        stroke: #009390;
    }

    #Stream1Legend, #Nebo1DLegend {
        fill: #009390;
    }

    #Stream2, #Nebo2D {
        stroke: #8dc63f;
    }

    #Stream2Legend, #Nebo2DLegend {
        fill: #8dc63f;
    }

    #Stream3, #Nebo1S {
        stroke: #132d46;
    }

    #Stream3Legend, #Nebo1SLegend {
        fill: #132d46;
    }

    #Stream4, #Nebo2S {
        stroke: #aaa813;
    }

    #Stream4Legend, #Nebo2SLegend {
        fill: #aaa813;
    }

    #Stream5, #Nebo3 {
        stroke: #619dd4;
    }

    #Stream5Legend, #Nebo3Legend {
        fill: #619dd4;
    }

    .pn1d, .pn2d {
      fill: none;
      clip-path: url(#clip);
    }

    .pn1d {
      stroke: #009390;
    }

    .pn2d {
      stroke: #1b4164;
    }

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

    .brush .extent {
      stroke: #fff;
      fill-opacity: .125;
      shape-rendering: crispEdges;
    }

    </style>
        </head>



    <body>
    <script>

    var marginFocus = {top: 10, right: 10, bottom: 250, left: 40},
        marginContext = {top: 430, right: 10, bottom: 170, left: 40},
        width = 960 - marginFocus.left - marginFocus.right,
        heightFocus = 650 - marginFocus.top - marginFocus.bottom,
        heightContext = 650 - marginContext.top - marginContext.bottom;
        legendOffset = 550;

    var parseDate = d3.time.format("%d/%m/%y %H:%M").parse;

    var xFocus = d3.time.scale().range([0, width]),
        xContext = d3.time.scale().range([0, width]),
        yFocus = d3.scale.linear().range([heightFocus, 0]),
        yContext = d3.scale.linear().range([heightContext, 0]);

    var xAxisFocus = d3.svg.axis().scale(xFocus).orient("bottom"),
        xAxisContext = d3.svg.axis().scale(xContext).orient("bottom"),
        yAxisFocus = d3.svg.axis().scale(yFocus).orient("left");

    var levelFocus = d3.svg.line()
        .interpolate("linear")
        .x(function(d) { return xFocus(d.date); })
        .y(function(d) { return yFocus(d.level); });


    var levelContext = d3.svg.line()
        .interpolate("linear")
        .x(function(d) { return xContext(d.date); })
        .y(function(d) { return yContext(d.level); });

    var svg = d3.select("body").append("svg")
        .attr("width", width + marginFocus.left + marginFocus.right)
        .attr("height", heightFocus + marginFocus.top + marginFocus.bottom);

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

    var focus = svg.append("g")
        .attr("class", "focus")
        .attr("transform", "translate(" + marginFocus.left + "," + marginFocus.top + ")");

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

    d3.csv("data/PiezoNeboNestSimple.csv", function(error, data) {
        data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.level = +d.level;
        });

      xFocus.domain(d3.extent(data.map(function(d) { return d.date; })));
      yFocus.domain([d3.min(data.map(function(d) { return d.level; })) -2,0]);
      xContext.domain(xFocus.domain());
      yContext.domain(yFocus.domain());

        // Nest the entries by piezo
        var dataNest = d3.nest()
            .key(function(d) {return d.piezo;})
            .entries(data);

        legendSpace = width/dataNest.length; // spacing for legend // ******

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


    focus.selectAll("g").data(dataNest)
        .enter()
        .append("g")
        .attr("class", "line")
        .attr("id", function(d) { return d.key.replace(/\s+/g, '') })  //the replace stuff is getting rid of spaces
        .append("path")
        .attr("d", function(d) { return levelFocus(d.values); });    

    context.selectAll("g").data(dataNest)
        .enter()
        .append("g")
        .attr("class", "line")
        .attr("id", function(d) { return d.key.replace(/\s+/g, '') })  //the replace stuff is getting rid of spaces
        .append("path")
        .attr("d", function(d) { return levelContext(d.values); });    


      focus.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + heightFocus + ")")
          .call(xAxisFocus);

      focus.append("g")
          .attr("class", "y axis")
          .call(yAxisFocus);

      context.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + heightContext + ")")
          .call(xAxisContext);

      context.append("g")
          .attr("class", "x brush")
          .call(brush)
        .selectAll("rect")
          .attr("y", -6)
          .attr("height", heightContext + 7);

    function brushed() {
      xFocus.domain(brush.empty() ? xContext.domain() : brush.extent());
      focus.selectAll(".line").attr("d", levelFocus(dataNest.values));
      focus.select(".x.axis").call(xAxisFocus);
    }

    });


    </script>
    </body>
    </html>
1

There are 1 answers

0
Lars Kotthoff On BEST ANSWER

It mostly boils down to two things as far as I can see. First, the elements you're selecting to be updated are the g and not the path elements and second, you need to reference the data bound to the elements in order to set d. Both are easily fixed and the brushed function looks something like this.

function brushed() {
   xFocus.domain(brush.empty() ? xContext.domain() : brush.extent());
   focus.selectAll("path").attr("d", function(d) { return levelFocus(d.values); });
   focus.select(".x.axis").call(xAxisFocus);
}

Complete demo here. Note that some bits are still missing, in particular the clip path to restrict the lines to the chart area. This can be copied and pasted directly from the example you've referenced though.