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?