0

Learning D3 I created a chart based off this example. The chart is implemented as a JS closure, with Mike Bostock’s convention for creating reusable components in D3. (or as close as I can get)

When zooming and panning, the line path is not redrawn correctly.

In my chart I have a scatter plot and a line path joining the dots. The dots work but not the line. It's (maybe) something to do with rebinding the xScale during the onzoom behavior.... I've tried exposing the line function / object and a bunch of trial and error stuff but am at my wits end. Any help much appreciated.

Please see this codepen, or run the embedded code snippet.

http://codepen.io/Kickaha/pen/epzNyw

var MyNS = MyNS || {};

MyNS.EvalChartSeries = function () {
    
    var xScale = d3.time.scale(),
        yScale = d3.scale.linear();
         //I tried exposing the line function / object to be able to call it in the on zoom ... no dice.  
        //var line = d3.svg.line();
    
    var EvalChartSeries = function (selection) {
        
        selection.each(function (dataIn) {
            //select and bind data for scatter dots
            spots = d3.select(this).selectAll("circle")
                .data(dataIn);             
            //enter and create a circle for any unassigned datum
            spots.enter().append("circle");
            
            //update the bound items using the x-y scale function to recalculate 
            spots
                            .attr("r", 8)
                .attr("cx", function (d) { return xScale(d.dt); })
                .attr("cy", function (d) { return yScale(d.spot); })
                            .style("fill", function (d) {
                switch (d.eva) {
                    case 1: return "green"; break;
                    case 2: return "red"; break;
                    case 3: return "blue"; break;
                    case 4: return "yellow"; break;}
            });
            
           //exit to remove any unused data, most likely not needed in this case as the data set is static
           spots.exit().remove();           
          
          //here the line function/object is assigned it's scale and bound to data
           var line = d3.svg.line().x(function (d) { return xScale(d.dt); })
                .y(function (d) { return yScale(d.spot); }).interpolate("linear");
          
          //and here is where the line is drawn by appending a set of svg path points
          //, it does not use the select, enter, update, exit logic because a line while being a set of points is one thing               (http://stackoverflow.com/questions/22508133/d3-line-chart-with-enter-update-exit-logic)
                   
            lines = d3.select(this)
            .append("path");
            
            lines
            .attr('class', 'line')
            .attr("d", line(dataIn))
            .attr("stroke", "steelblue").attr("stroke-width", 1);
        });

    };
    
    //The scales are exposed as properties, and they return the object to support chaining
    EvalChartSeries.xScale = function (value) {
        if (!arguments.length) {
            return xScale;
        }
        xScale = value;
        return EvalChartSeries;
    };
    
    EvalChartSeries.yScale = function (value) {
        if (!arguments.length) {
            return yScale;
        }
        yScale = value;
        return EvalChartSeries;
    };

  /*
  Here I tried to expose the line function/object as a property to rebind it to the xAxis when redrawing ... didnt work 
    EvalChartSeries.line = function (value) {
        if (!arguments.length) {
            return line;
        }
        line = value;
        //linePath.x = function (d) { return xScale(d.dt); };
        return EvalChartSeries;
    };*/
  
    //the function returns itself to suppport method chaining  
    return EvalChartSeries;

};

//The chart is defined here as a closure to enable Object Orientated reuse (encapsualtion / data hiding etc.. )
MyNS.DotsChart = (function () {

  data = [{"dt":1280780384000,"spot":1.3173999786376953,"eva":4},
{"dt":1280782184000,"spot":1.3166999816894531,"eva":4},
{"dt":1280783084000,"spot":1.3164000511169434,"eva":4},
{"dt":1280781284000,"spot":1.3167999982833862,"eva":4},
{"dt":1280784884000,"spot":1.3162000179290771,"eva":4},
{"dt":1280783984000,"spot":1.3163000345230103,"eva":4},
{"dt":1280785784000,"spot":1.315999984741211,"eva":4},
{"dt":1280786684000,"spot":1.3163000345230103,"eva":4},
{"dt":1280787584000,"spot":1.316100001335144,"eva":4},
{"dt":1280788484000,"spot":1.3162000179290771,"eva":4},
{"dt":1280789384000,"spot":1.3164000511169434,"eva":4},
{"dt":1280790284000,"spot":1.3164000511169434,"eva":4},
{"dt":1280791184000,"spot":1.3166999816894531,"eva":4},
{"dt":1280792084000,"spot":1.3169000148773193,"eva":4},
{"dt":1280792984000,"spot":1.3170000314712524,"eva":4},
{"dt":1280793884000,"spot":1.3174999952316284,"eva":4},
{"dt":1280794784000,"spot":1.3171000480651855,"eva":4},
{"dt":1280795684000,"spot":1.3163000345230103,"eva":2},
{"dt":1280796584000,"spot":1.315600037574768,"eva":2},
{"dt":1280797484000,"spot":1.3154000043869019,"eva":2},
{"dt":1280798384000,"spot":1.3147000074386597,"eva":2},
{"dt":1280799284000,"spot":1.3164000511169434,"eva":2},
{"dt":1280800184000,"spot":1.3178000450134277,"eva":4},
{"dt":1280801084000,"spot":1.3176000118255615,"eva":4},
{"dt":1280801984000,"spot":1.3174999952316284,"eva":4},
{"dt":1280802884000,"spot":1.3193000555038452,"eva":3},
{"dt":1280803784000,"spot":1.32260000705719,"eva":4},
{"dt":1280804684000,"spot":1.3216999769210815,"eva":4},
{"dt":1280805584000,"spot":1.3233000040054321,"eva":4},
{"dt":1280806484000,"spot":1.3229000568389893,"eva":4},
{"dt":1280807384000,"spot":1.3229999542236328,"eva":2},
{"dt":1280808284000,"spot":1.3220000267028809,"eva":2},
{"dt":1280809184000,"spot":1.3224999904632568,"eva":2},
{"dt":1280810084000,"spot":1.3233000040054321,"eva":2},
{"dt":1280810984000,"spot":1.3240000009536743,"eva":2},
{"dt":1280811884000,"spot":1.3250000476837158,"eva":4},
{"dt":1280812784000,"spot":1.3253999948501587,"eva":4},
{"dt":1280813684000,"spot":1.3248000144958496,"eva":4},
{"dt":1280814584000,"spot":1.3250000476837158,"eva":4},
{"dt":1280815484000,"spot":1.3249000310897827,"eva":4},
{"dt":1280816384000,"spot":1.3238999843597412,"eva":2},
{"dt":1280817284000,"spot":1.3238999843597412,"eva":2},
{"dt":1280818184000,"spot":1.322700023651123,"eva":2},
{"dt":1280819084000,"spot":1.32260000705719,"eva":2},
{"dt":1280819984000,"spot":1.3219000101089478,"eva":2},
{"dt":1280820884000,"spot":1.323199987411499,"eva":4},
{"dt":1280821784000,"spot":1.3236000537872314,"eva":4},
{"dt":1280822684000,"spot":1.3228000402450562,"eva":4},
{"dt":1280823584000,"spot":1.3213000297546387,"eva":2},
{"dt":1280824484000,"spot":1.3214999437332153,"eva":2},
{"dt":1280825384000,"spot":1.3215999603271484,"eva":2},
{"dt":1280826284000,"spot":1.320199966430664,"eva":2},
{"dt":1280827184000,"spot":1.3187999725341797,"eva":2},
{"dt":1280828084000,"spot":1.3200000524520874,"eva":2},
{"dt":1280828984000,"spot":1.3207000494003296,"eva":1}
  ];
  
  
   var minDate = d3.min(data, function (d) { return d.dt; }),
   maxDate = d3.max(data, function (d) { return d.dt; });    
   var yMin = d3.min(data, function (d) { return d.spot; }),
   yMax = d3.max(data, function (d) { return d.spot; });

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Set up the drawing area
    
    var margin = {top: 20, right: 20, bottom: 30, left: 35},
        width = 1600 - margin.left - margin.right,
        height = 400 - margin.top - margin.bottom;    

    //select the single element chart in the html body (this is expected to exist) and append a svg element
    var plotChart =d3.select('#chart')
    .append("svg:svg")
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
        .append('svg:g')
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    var plotArea = plotChart.append('g')
        .attr('clip-path', 'url(#plotAreaClip)');//http://stackoverflow.com/questions/940451/using-relative-url-in-css-file-what-location-is-it-relative-to
    
    plotArea.append('clipPath')
        .attr('id', 'plotAreaClip')
        .append('rect')
        .attr({ width: width, height: height });

    // Scales
    var xScale = d3.time.scale(),
        yScale = d3.scale.linear();

    // Set scale domains
    xScale.domain([minDate, maxDate]);
    yScale.domain([yMin, yMax]).nice();

    // Set scale ranges
    xScale.range([0, width]);
    yScale.range([height, 0]);

    // Axes
    var xAxis = d3.svg.axis()
        .scale(xScale)
        .orient('bottom')
        .ticks(5);

    var yAxis = d3.svg.axis()
        .scale(yScale)
        .orient('left');
    
    
  /*  var line = d3.svg.line()
                .x(function (d) { return xScale(d.dt); })
                .y(function (d) { return yScale(d.spot); }).interpolate("linear");
    */

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

    plotChart.append('g')
        .attr('class', 'y axis')
        .call(yAxis);

    // Data series
    var series = MyNS.EvalChartSeries()
        .xScale(xScale)
        .yScale(yScale);
       // .line(line); exposing this property did nothing 
        
    //appending a group 'g' tag binding the data and calling on our d3 line+dots chart object to process it    
    var dataSeries = plotArea.append('g')
        .attr('class', 'series')
        .datum(data)
        .call(series);


    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Zooming and panning
    //on zoom check extents , then most importantny redraw the chart
    var zoom = d3.behavior.zoom()
        .x(xScale)
        .on('zoom', function() {
            if (xScale.domain()[0] < minDate) {
                zoom.translate([zoom.translate()[0] - xScale(minDate) + xScale.range()[0], 0]);
            } else if (xScale.domain()[1] > maxDate) {
                zoom.translate([zoom.translate()[0] - xScale(maxDate) + xScale.range()[1], 0]);
            }
            //most important to redraw "on zoom" 
            redrawChart();            
        });

    //an overlay area to catch mouse events from the full area of the chart (not just the rendered dots and line)
    var overlay = d3.svg.area()
        .x(function (d) { return xScale(d.dt); })
        .y0(0)
        .y1(height);
    //an area is a path object, not to be confused with our line path 
    plotArea.append('path')
        .attr('class', 'overlay')
        .attr('d', overlay(data))
 .call(zoom);

    redrawChart();    
    updateZoomFromChart();

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Helper methods

    function redrawChart() {
        //redraws the scatter data series
        dataSeries.call(series);
        //redraws the xaxis to show the current zoom pan area
        plotChart.select('.x.axis').call(xAxis);
        
     //   plotChart.select(".line")
      //  .attr("class", "line");
      //  .attr("d", line);

     
    //filters the data set to what is visible given teh current zoom pan state
      var yExtent = d3.extent(data.filter(function (d) {
            var dt = xScale(d.dt);
            return dt > 0 && dt < width;
        }), function (d) { return d.spot; });
        
        yScale.domain(yExtent).nice();        
      //this scales the y axis to maximum visibility as the line is zoomed and panned
        plotChart.select(".y.axis").call(yAxis);
    }       
   //takes care of zooming and panning past the ends of the data.
    function updateZoomFromChart() {
        var fullXDomain = maxDate - minDate,
            currentXDomain = xScale.domain()[1] - xScale.domain()[0];
        var minXScale = currentXDomain / fullXDomain,
            maxXScale = minXScale * 20;
        zoom.x(xScale)
            .scaleExtent([minXScale, maxXScale]);

    }})()
#chart {
    margin-top: 20px;
    margin-bottom: 20px;
    width: 660px;
}.chart .overlay {             
    stroke-width: 0px;
    fill-opacity: 0;
}
.overlay {             
    stroke-width: 0px;
    fill-opacity: 0;
}


body {
  padding: 10px 20px;
  background: #ffeeee;
  font-family: sans-serif;
  text-align: center;
  color: #7f7;
}.line {
    fill: none;
    stroke: steelblue;
    stroke-width: 1.5px;
}

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

.axis text {
    font-family: sans-serif;
    font-size: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

  <div id="chart"></div>

How do I get the line to redraw correctly?

Kickaha
  • 3,680
  • 6
  • 38
  • 57

1 Answers1

1

Thank you for a very well documented question.

What you are doing, is that on zoom you re-draw the line, without removing the one already existing in your SVG element. May I suggest the following:

Change your zoom method to:

var zoom = d3.behavior.zoom()
    .x(xScale)
    .on('zoom', function() {
        if (xScale.domain()[0] < minDate) {
            zoom.translate([zoom.translate()[0] - xScale(minDate) + xScale.range()[0], 0]);
        } else if (xScale.domain()[1] > maxDate) {
            zoom.translate([zoom.translate()[0] - xScale(maxDate) + xScale.range()[1], 0]);
        }
      // add the following line, to remove the lines already present
      d3.selectAll('.line').remove()
        //most important to redraw "on zoom" 
        redrawChart();            
    });

I am sure there are better ways of doing it, but I think this will get you started.

Hope it helps.

Nikos
  • 3,267
  • 1
  • 25
  • 32
  • Thank you kindly. I'm not so sure as you that there is a better way than removing the 'ghost' !..., it's exactly what was needed. – Kickaha Sep 20 '15 at 08:40