Related
Inspired by https://www.nytimes.com/interactive/2018/03/19/upshot/race-class-white-and-black-men.html
I am trying to create a animation that will have markers move from one point to multiple levels in another point in the y axis. Thanks to detailed pages by Amelia and Mike Bostock in both bl.ocks.org and stackoverflow. I have got so far to get the circles and animate it. But, I am not able to make each marker loop over by the pathlevel and do the transition
Pathlevel, here, indicates whether they are high, middle or low (1,2,3).
The entire code in using d3.v4 has been pasted below. what I am missing?
Thanks for your help.
<!DOCTYPE html>
<meta charset="utf-8">
<title>SANKEY Experiment</title>
<style>
</style>
<body>
<script type="text/javascript" src="d3.v4.js"></script>
<script>
//ref: very important for path animation: https://stackoverflow.com/questions/21226523/trace-path-with-dom-object/21228701#21228701
//ref: clustered force layout: https://bl.ocks.org/mbostock/7881887
//ref: data manipulation: http://learnjsdata.com/iterate_data.html
var margin = {top: 30, right: 10, bottom: 30, left: 20},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var series = [{x:5,y:10},{x:150,y:10}],
series2 = [{x:5,y:10},{x:50,y:15},{x:100,y:30},{x:150,y:30}],
series3 = [{x:5,y:10},{x:50,y:22},{x:100,y:50},{x:150,y:50}];
var data = [{"dim":"a","pos":"high","pathlevel":1,"x1":1,"y1":10,"x2":150,"y2":8},
{"dim":"b","pos":"high","pathlevel":1,"x1":1,"y1":10,"x2":150,"y2":8},
{"dim":"a","pos":"mid","pathlevel":2,"x1":1,"y1":10,"x2":150,"y2":28},
{"dim":"b","pos":"mid","pathlevel":2,"x1":1,"y1":10,"x2":150,"y2":28},
{"dim":"a","pos":"low","pathlevel":3,"x1":1,"y1":10,"x2":150,"y2":48},
{"dim":"b","pos":"low","pathlevel":3,"x1":1,"y1":10,"x2":150,"y2":48}]
var x = d3.scaleLinear()
.domain([5,150])
.range([0,width]);
var y = d3.scaleLinear()
.domain([10,50])
.range([0,height]);
var line = d3.line()
.curve(d3.curveCardinal)
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
var svg = d3.select("body").append("svg").attr("height",600).attr("width",600);
var chart = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
var update = function(series,k){
chart.append("path")
.attr("class","route"+k)
.attr("fill","none")
.attr("stroke","blue")
.attr("d",line(series));
}
update(series,1);
update(series2,2);
update(series3,3);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//create transistions along the path //////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var colorScale = d3.scaleOrdinal()
.domain(['a','b'])
.range(['orange','darkblue']);
//Get path start point for placing marker
var path = d3.select('.route1')
var startPoint = path.attr("d").split(",")[1];
//path example["M12.885906040268456", "84.48979591836735C12.885906040268456", "84.48979591836735", "241.07382550335572", "84.48979591836735",
//"318.9261744966443", "84.48979591836735C396.7785234899329", "84.48979591836735", "480", "84.48979591836735", "480", "84.48979591836735"]
//selecting class route which represents the path. d represents the path that is held by the path object. in that we split by comma and take the first
console.log(startPoint);
var glider = function(data,p){//p for path level
var marker = chart.selectAll(".marker")
.data(data)
.enter().append('circle')
.attr("class","marker")
.attr("fill",function(d){ return colorScale(d.dim);})
//.attr("x",function(d,i){return x(d.x1)+i*10;})
//.attr("y",function(d,i){return y(d.y1)+i*10;})
.attr("r",5);
//.attr("width",10)
//.attr("height",10);
var simulation = d3.forceSimulation()
.force("x",d3.forceX().strength(0.05))
.force("y",d3.forceY().strength(0.01))
.force('charge', d3.forceManyBody().strength(20))
.force("collide",d3.forceCollide(function(d){return y(d.y1)+4;}))
.alphaTarget(.03)
.restart();
simulation.nodes(data)
.on('tick',ticked);
function ticked(){
marker
.attr("cx",function(d){ return d.x;})
.attr("cy",function(d){ return d.y;})
}//end of ticked
//marker.transition().duration(3000).delay(200)
// .attr("x",function(d,i){return x(d.x2)+i*10;});
function translateAlong(path) {
var l = path.getTotalLength();
return function (d) {
return function (t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}//end of translateAlong
console.log(marker);
function transition(){
var path2 = d3.select('.route'+p);
marker.attr("transform", "translate(" + startPoint + ")").transition().duration(3000).delay(function(d,i) { return i * 100; })
.attrTween("transform",translateAlong(path2.node()));
//.attr("x",function(d,i){return x(d.x2)+i*10;});
}//end of transition
transition();
}
/*var check = d3.map(data, function(d){return d.pathlevel;}).keys(); //for getting unique values from a column
check.forEach(function(i){
datapoints = data.filter(function(d){return d.pathlevel==i});
console.log(i);
glider(datapoints,i);
});*/
data1 = data.filter(function(d){return d.pathlevel==1});
data2 = data.filter(function(d){return d.pathlevel==2});
data3 = data.filter(function(d){return d.pathlevel==3});
//glider(data1,1);
//glider(data2,2);
glider(data3,3);
//glider(data,2);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*function createPathTween(d, i, a) {
var path = this.parentNode.getElementsByTagName("path")[1];
//i.e., go from this <circle> -> parent <g> -> array of child <path> elements
//-> first (and only) element in that array
//console.log(path);
var l = path.getTotalLength();
return function(t) {
var p = path.getPointAtLength(t * l);
console.log(p);
return "translate(" + p.x + "," + p.y + ")";
};
}//end of tweenpath*/
</script>
</body>
This is probably a pretty specific question:
My problem is that in d3.js i need to create a radial chart.
I created the axis and labels.
Now i want to draw the radialLine.
It creates the path objects in my HTML document,
but without any coordinates.
I think it has something to do with the way the radius/data is provided to the radialLine, but can't figure out what to change...
Hopefully someone sees my mistake.
I also created a JSfiddle:
complete JSfiddle
//Data:
var notebookData = [{
model: "Levecchio 620RE",
data: [579, 8, 2.4, 256, 13.3]
}];
var categories = [
"Price",
"RAM",
"CPU",
"Storage",
"Display"
];
var priceScale = d3.scaleLinear().domain([2500,300]).range([0,100]);
var ramScale = d3.scaleLinear().domain([0,32]).range([0,100]);
var cpuScale = d3.scaleLinear().domain([1.0,3.2]).range([0,100]);
var storageScale = d3.scaleLinear().domain([64,2048]).range([0,100]);
var displaySizeScale = d3.scaleLinear().domain([10.0,20.0]).range([0,100]);
function selectScale(category_name) {
switch(category_name) {
case "Price":
return priceScale;
case "RAM":
return ramScale;
case "CPU":
return cpuScale;
case "Storage":
return storageScale;
case "Display":
return displaySizeScale;
}
}
var scaledData = notebookData.map(function (el) {
return el.data.map(function (el2, i) { //el = 1 notebook
return selectScale(categories[i])(el2);
});
});
//My RadialLine
//generatorfunction
var radarLine = d3.radialLine()
.radius(function(d) { return scaledData(d.value); })
.angle(function(d,i) { return i*angleSlice; })
.curve(d3.curveLinearClosed)
;
//Create the wrapper
var radarWrapper = g.selectAll(".radarWrapper")
.data(notebookData)
.enter().append("g")
.attr("class", "radarWrapper")
;
//Create pathlines
radarWrapper.append("path")
.attr("class", "radarStroke")
.attr("d", function(d,i) { return radarLine(d); })
.style("stroke-width", cfg.strokeWidth + "px")
.style("stroke", function(d,i) { return cfg.color(i); })
.style("fill", "none")
;
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I've edited your fiddle a bit to make it work:
https://jsfiddle.net/2qgygksL/75/
Basicly what i've done:
fix the color scheme
var colors = d3.scale.category10();
instead of
var colors = d3.scale.ordinal(d3.schemeCategory10);
added data to path
radarWrapper.append("path")
.data(scaledData)
change radius to
.radius(function(d, i) {
return d;
})
since You used something like return scaledData(d.value); where your scaledData is an array.
I'm new to js and have read that there are "monsters lurking in the shadows" when it comes to the use of variables and their respective scope and how hidden data is.
I'm using d3.js and loading data in from a csv but I only want to make a GET call once as more complex visualisations may involve 10 csv files some containing upto 10,000 rows of data - therefore I've added a variable at the top of my script "var data;" which gets filled on initial rendering then the variable gets used on susequent interations from the user.
Is my approach safe? If there is a better pattern I should use what is it?
BarData.csv looks like this:
Fruit,dt,amount
Apple,12/28/2016,-1256
Apple,12/29/2016,-500
Apple,12/30/2016,3694
Apple,12/31/2016,5586
Apple,1/1/2017,4558
Apple,1/2/2017,6696
Apple,1/3/2017,7757
Apple,1/4/2017,8528
Apple,1/5/2017,5543
Apple,1/6/2017,3363
Apple,1/7/2017,5464
Pear,12/25/2017,250
Pear,12/26/2017,669
Pear,12/27/2017,441
Pear,12/28/2017,159
Pear,12/29/2017,357
Pear,12/30/2017,775
Pear,12/31/2017,669
The code is all in a single html file as follows:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>BAR SINGLE FUNCTION</title>
<script src="http://d3js.org/d3.v3.js"></script>
<style type="text/css">
#radioDiv {
top: 45px;
font-family: verdana;
font-size: 8px;
width: 455px;
}
#TOPbarChart {
position: absolute;
top: 50px;
left: 30px;
width: 750px;
height: 195px;
}
.axis--y path,
.axis--x path {
display: none;
}
.axis--x line,
.axis--y line {
stroke: black;
fill: none;
stroke-width: 2px
}
.yAxis text,
.xAxis text {
font: 7pt Verdana;
stroke: none;
fill: black;
}
.title,
.titleX {
font-family: Verdana;
font-size: 10px;
}
</style>
</head>
<body>
<div id="radioDiv">
<label>
<input id="radioFrt" type="radio" name="frt" value="Apple" class="radioB" checked> APPLE
</label>
<label>
<input type="radio" name="frt" value="Pear" class="radioB"> PEAR
</label>
</div>
<div id="TOPbarChart"></div>
<script type="text/javascript">
var currentFruit = "Apple";
var currentColr = "#00a5b6";
var barDataCSV_Dly = "BarData.csv";
var data;
//
//
// radio button
document.getElementById("radioFrt").checked = true;
d3.selectAll('input[name="frt"]').on("change", function change() {
currentFruit = this.value;
//load();
TOPbarChart(currentFruit, currentColr);
});
//FORMATS
var parseDate = d3.time.format("%m/%d/%Y").parse;
//
// BASIC SIZING
//
function barChartBasics() {
var margin = {
top: 25,
right: 35,
bottom: 25,
left: 70
},
width = 550 - margin.left - margin.right,
height = 155 - margin.top - margin.bottom,
colorBar = d3.scale.category20(),
barPaddingFine = 1,
barPaddingThick = 2;
return {
margin: margin,
width: width,
height: height,
colorBar: colorBar,
barPaddingFine: barPaddingFine,
barPaddingThick: barPaddingThick
};
}
// create svg element
var basics = barChartBasics();
var svg = d3.select("#TOPbarChart")
.append("svg")
.attr({
"width": basics.width + basics.margin.left + basics.margin.right,
"height": basics.height + basics.margin.top + basics.margin.bottom,
id: "svgTOPbarChart"
});
// create svg group
var plot = svg
.append("g")
.attr({
"transform": "translate(" + basics.margin.left + "," + basics.margin.top + ")",
id: "svgPlotTOPbarChart"
});
var axisPadding = 2;
var leftAxisGroup = svg
.append('g')
.attr({
transform: 'translate(' + (basics.margin.left - axisPadding) + ',' + (basics.margin.top) + ')',
'class': "yAxis axis--y",
id: "yAxisGTOPbarChart"
});
var bottomAxisGroup = svg
.append('g')
.attr({
'class': "xAxis axis--x",
id: "xAxisGTOPbarChart"
});
var titleTxt = svg.append("text")
.attr({
x: basics.margin.left + 12,
y: 20,
'class': "title",
'text-anchor': "start"
})
// create scales with ranges
var xScale = d3.time.scale().range([0, basics.width]);
var yScale = d3.scale.linear().range([basics.height, 0]);
function load(){
d3.csv(barDataCSV_Dly, function(rows) {
data = rows;
TOPbarChart(currentFruit, currentColr)
})
}
function TOPbarChart(
frt, colorChosen) {
// get the data
//d3.csv(barDataCSV_Dly, function(rows) {
TOPbarData = data.map(function(d) {
return {
"Fruit": d.Fruit,
"dt": parseDate(d.dt),
"amount": +d.amount
};
}).filter(function(row) {
if (row['Fruit'] == frt) {
return true;
}
});
// create domains for the scales
xScale.domain(d3.extent(TOPbarData, function(d) {
return d.dt;
}));
var amounts = TOPbarData.map(function(d) {
return d.amount;
});
var yMax = d3.max(amounts);
var yMin = d3.min(amounts);
var yMinFinal = 0;
if (yMin < 0) {
yMinFinal = yMin;
}
yScale.domain([yMinFinal, yMax]);
// introduce the bars
// var plot = d3.select("#svgPlotTOPbarChart")
var sel = plot.selectAll("rect")
.data(TOPbarData);
sel.enter()
.append("rect")
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(Math.max(0, d.amount));
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return Math.abs(yScale(d.amount) - yScale(0));
},
//fill: colorChosen,
fill: function(d, i) {
return d.amount < 0 ? "#FF0000" : colorChosen;
},
// fill: function(d, i) {
// var col = colorChosen
// if (d.amount < 0) {
// col = "#FF0000";
// }
// return col;
// },
'class': "bar"
});
// this little function will create a small ripple affect during transition
var dlyRipple = function(d, i) {
return i * 100;
};
sel
.transition()
.duration(dlyRipple) //1000
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(Math.max(0, d.amount));
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return Math.abs(yScale(d.amount) - yScale(0));
},
//fill: colorChosen
fill: function(d, i) {
var col = colorChosen
if (d.amount < 0) {
col = "#FF0000";
}
return col;
},
});
sel.exit().remove();
// add/transition y axis - with ticks and tick markers
var axisY = d3.svg.axis()
.orient('left')
.scale(yScale)
.tickFormat(d3.format("s")) // use abbreviations, e.g. 5M for 5 Million
.outerTickSize(0);
leftAxisGroup.transition().duration(1000).call(axisY);
// add/transition x axis - with ticks and tick markers
var axisX = d3.svg.axis()
.orient('bottom')
.scale(xScale);
bottomAxisGroup
.attr({
transform: 'translate(' + (basics.margin.left + ((basics.width / TOPbarData.length) / 2)) + ',' + (basics.margin.top + basics.height) + ')',
})
.transition().duration(1000).call(axisX.ticks(5));
titleTxt.text("Daily: last " + TOPbarData.length + " days");
// console.log(TOPbarData.length)
//});
}
//
//
//
//
load()
//TOPbarChart(currentFruit, currentColr);
//
//
//
//
</script>
</body>
</html>
Working example here on Plunker:
http://plnkr.co/edit/lfWgibRsm6mMzLYSgR2o?p=info
Edit
I tried Piotr's suggestion so I now have this:
var Module_BarDataDaily = (function() {
var data;
d3.csv("myData.csv", function(rows) {
data = rows;
});
return {
dataX: data
}
})();
Then in a subsequent function I have the following:
var x = Module_BarDataDaily.dataX;
TOPbarData = x.map(function(d) { /<<< Type error x is undefined
return {
...
It is throwing an undefined error at the line marked above.
Caching records in variable is not bad until it's private and not visible anywhere else in scripts.
Your current approach sets data variable in global namespace, essentially if any other plugin you include will also have variable called data, might override it's value. - That is not safe.
To hide variable from rest of the world, you would want to include all your code in self-invoking function:
(function() {
var data = ...; // It's accessible only in that function
// Your JavaScript code
})();
Related notes:
Benefits of self-invoking function
How scope works in JavaScript
If you want to share variable between files, do it within object (called module), e.g.:
var Module = (function() {
var data = ...
// Your JavaScript code
return {
data : data
}
})();
// Now it's accessible with Module.data
JavaScript provides build-in module system, that is similar to Python. However it's not yet supported in browsers.
Related notes:
Module pattern described
ES2015 import modules
PS: Separate your CSS and JavaScript to external files to avoid mess.
After update your question, there is another problem called asynchronous data.
Library function d3.csv(...) works asynchronous from other calculations, which means you can't never be sure that data is filled. It might be immediately, or it might be after few seconds.
That's why you get x is undefined, because in moment of parsing that code, d3.csv didn't finished calculations yet.
The easiest attempt to handle this code is to use callback function.
Basically, your module returns function loadData that accepts function which will be invoked only when module will know that data is filled.
var Module_BarDataDaily = (function() {
var data;
return {
loadData: lazyLoadData
};
function lazyLoadData(callback) {
if (data) {
callback(data); // data already exists, run function
} else {
d3.csv("myData.csv", function(rows) {
data = rows;
callback(data); // data is filled, run function
});
}
}
})();
Here is how it can be used with that attempt.
Module_BarDataDaily.loadData(function(data) {
// this function loads only after data is filled
TopChartData = data.map(...);
});
What is callback?
In plain English, a callback function is like a Worker who "calls
back" to his Manager when he has completed a Task.
Source: How to explain callbacks in plain english? How are they different from calling one function from another function?
Note: There are many other, potentially better attempts to solve such problems like Promise from ES2015, await that will be released with ES2017.
I want to use d3.chart() for the charts I have written already. I found examples of d3.chart() for circle and barcharts but not for line charts. My charts are line charts, I need to use following code in d3.charts()
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
but am facing problem when try to use like this
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="d3.v3.min.js"></script>
<script type="text/javascript" src="d3.chart.min.js"></script>
</head>
<body>
<div id="vis"></div>
<script type="text/javascript">
d3.chart("linechart", {
initialize: function() {
// create a base scale we will use later.
var chart = this;
chart.w = chart.base.attr('width') || 200;
chart.h = chart.base.attr('height') || 150;
chart.w = +chart.w;
chart.h = +chart.h;
chart.x = d3.scale.linear()
.range([0, chart.w]);
chart.y = d3.scale.linear()
.range([chart.h,0]);
chart.base.classed('line', true);
this.areas = {};
chart.areas.lines = chart.base.append('g')
.classed('lines', true)
.attr('width', chart.w)
.attr('height', chart.h)
chart.line = d3.svg.line()
.x(function(d) { return chart.x(d.x);})
.y(function(d) { return chart.y(d.y);});
this.layer("lines", chart.areas.lines, {
dataBind: function(data) {
// update the domain of the xScale since it depends on the data
chart.y.domain([d3.min(data,function(d){return d.y}),d3.max(data,function(d){return d.y})])
chart.x.domain(d3.extent(data, function(d) { return d.x; }));
// return a data bound selection for the passed in data.
return this.append("path")
.datum(data)
.attr("d", chart.line)
.attr('stroke','#1ABC9C')
.attr('stroke-width','2')
.attr('fill','none');
},
insert: function() {
return null;
},
});
},
// configures the width of the chart.
// when called without arguments, returns the
// current width.
width: function(newWidth) {
if (arguments.length === 0) {
return this.w;
}
this.w = newWidth;
return this;
},
// configures the height of the chart.
// when called without arguments, returns the
// current height.
height: function(newHeight) {
if (arguments.length === 0) {
return this.h;
}
this.h = newHeight;
return this;
},
});
var data = [
{x: 0,y:190},
{x: 1,y:10},{x: 2,y:40},{x: 3,y:90},
{x: 4,y:30},{x: 5,y:20},{x: 6,y:10}
];
var chart1 = d3.select("#vis")
.append("svg")
.chart("linechart")
.width(720)
.height(320)
chart1.draw(data);
</script>
</body>
</html>
error:
Uncaught Error: [d3.chart] Layer selection not properly bound.
I have get the line and error as well.
Note: Get d3.chart.min.js from this link
Get d3.v3.min.js from this link
Updated: I got answer from #LarsKotthoff answer, but there is different in image. check this links Before apply D3 and After apply D3.
It looks like you have confused the insert and dataBind actions -- in the former, you're supposed to append the new elements while the latter only binds the data. With the modifications below, your code works fine for me.
dataBind: function(data) {
// update the domain of the xScale since it depends on the data
chart.y.domain([d3.min(data,function(d){return d.y}),d3.max(data,function(d){return d.y})])
chart.x.domain(d3.extent(data, function(d) { return d.x; }));
// return a data bound selection for the passed in data.
return this.selectAll("path").data([data]);
},
insert: function() {
return this.append("path")
.attr("d", chart.line)
.attr('stroke','#1ABC9C')
.attr('stroke-width','2')
.attr('fill','none');
}
Note that this won't work for several lines -- to do that, change .data([data]) to .data(data) and use a nested array, e.g. [[{x:0,y:0},...], [{x:1,y:1},...], ...].
I am working on a time series line chart that lets the user scroll back from the present. I can find tutorials on real-time d3.js charts, I can find tutorials on zooming and panning, and I can find tutorials on using external data sources. I'm having trouble putting all this knowledge together.
Here is the behavior that I am looking for:
The chart can pan backward in time (meaning that the lines, data points, and axes move with dragging of the mouse or finger)
Panning should only effect the x-axis, and no zooming should occur.
As the user pans the chart, more data loads in, giving an experience of infinite scrolling
I plan on buffering in at least one extra "page" worth of data for the user to scroll into (already got this part figured out)
I don't think I need transitions, because the panning of the chart will already smoothly translate it
This is what I have working so far:
// set up a zoom handler only for panning
// by limiting the scaleExtent
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 1])
.on("zoom", pan);
var loadedPage = 1; // begin with one page of data loaded
var nextPage = 2; // next page will be page 2
var panX = 0;
function pan()
{
if (d3.event)
{
panX = d3.event ? d3.event.translate[0] : 0;
// is there a better way to determine when
// to load the next page?
nextPage = panX / (width + margin.left + margin.right) + 2;
nextPage = Math.floor(nextPage);
// if we haven't loaded in the next page's data
// load it in so that the user can scroll into it
if (nextPage > loadedPage) {
console.log("Load a new page");
loadedPage += 1;
// load more data
Chart.query( /*params will be here*/ ).then(
function(response) {
// append the new data onto the front of the array
data = data.concat(response);
console.log(data.length);
// I need to add the new data into the line chart
// but how do I make that work with the pan
// logic from zoom?
}
);
}
// is this where I update the axes and scroll the chart?
// What's the best way to do that?
}
}
In this code, I can know when to pull more data from the server, but I'm not sure how to insert the data into the chart in a way that works with the pan offset. Do I use transform translate, or can I update the d value of the path of my line?
Any suggestions would be welcome... also, if anyone knows of any demos which already show panning infinitely through time series data, that would be much appreciated.
As mentioned in the other answer, I know this is a very old post but hopefully the following will help someone...
I made a pen that I think hits all the requirements mentioned. As I didn't have a real API to use, I created some data using a json-generator (great tool), included it, and sorted it in descending order. Then I use the built in slice and concat methods to take bits of the array, data, and add to the chart_data variable (similarly to how one might use an api).
Important Sections:
Once you've created your scales, axes, and points (lines, bars, etc.), you need to create the zoom behavior. As mentioned in the question, keeping the scaleExtent limited to the same number on both sides prevents zooming:
var pan = d3.behavior.zoom()
.x(x_scale)
.scale(scale)
.size([width, height])
.scaleExtent([scale, scale])
.on('zoom', function(e) { ... });
Now that we've created the behavior, we need to call it. I'm also calculating what the x translation will be for this moment in time, now, and programmatically panning there:
// Apply the behavior
viz.call(pan);
// Now that we've scaled in, find the farthest point that
// we'll allow users to pan forward in time (to the right)
max_translate_x = width - x_scale(new Date(now));
viz.call(pan.translate([max_translate_x, 0]).event);
Both preventing the user from scrolling past now and loading more data is all done in the zoom event handler:
...
.scaleExtent([scale, scale])
.on('zoom', function(e) {
var current_domain = x_scale.domain(),
current_max = current_domain[1].getTime();
// If we go past the max (i.e. now), reset translate to the max
if (current_max > now)
pan.translate([max_translate_x, 0]);
// Update the data & points once user hits the point where current data ends
if (pan.translate()[0] > min_translate_x) {
updateData();
addNewPoints();
}
// Redraw any components defined by the x axis
x_axis.call(x_axis_generator);
circles.attr('cx', function(d) {
return x_scale(new Date(d.registered));
});
});
The other functions are pretty straightforward and can be found at the bottom of the pen. I'm not aware of any built in D3 function to prevent panning past the present but I'm definitely open to feedback if I've missed an easier way to do some of this.
Let me know if you have trouble viewing the pen or need clarification on something. If I have time I'll update this with another version demoing an infinite scrolling line chart.
P.S. In the pen, I'm consoling out the selection and data as they update. I suggest opening the console to see exactly what's happening.
This is too late, but answering just in case somebody needs again. I was having most of the code ready for my scatterplot so uploading that. Hope it helps you. The code is created as a trial when I was learning this features. So please check before you use.
Note:
D3js panning implemented with zoom behavior,
zooming disabled with scaleExtent,
Y panning restricted.
Data loaded when x extremes are reached.
Please check the Plunkr link
// Code goes here
window.chartBuilder = {};
(function(ns) {
function getMargin() {
var margin = {
top: 20,
right: 15,
bottom: 60,
left: 60
};
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
return {
margin: margin,
width: width,
height: height
};
}
function getData() {
var data = [
[5, 3],
[10, 17],
[15, 4],
[2, 8]
];
return data;
}
//function defineScales(data, width, height) {
// var x = d3.scale.linear()
// .domain([0, d3.max(data, function (d) {
// return d[0];
// })])
// .range([0, width]);
//
// var y = d3.scale.linear()
// .domain([0, d3.max(data, function (d) {
// return d[1];
// })])
// .range([height, 0]);
// return {x: x, y: y};
//}
function defineYScale(data, domain, range) {
var domainArr = domain;
if (!domain || domain.length == 0) {
domainArr = [0, d3.max(data, function(d) {
return d[1];
})];
}
var y = d3.scale.linear()
.domain(domainArr)
.range(range);
return y;
}
function defineXScale(data, domain, range) {
var domainArr = domain;
if (!domain || domain.length == 0) {
domainArr = [d3.min(data, function(d) {
return d[0];
}), d3.max(data, function(d) {
return d[0];
})];
}
var x = d3.scale.linear()
.domain(domainArr)
.range(range);
return x;
}
function getSvg(width, margin, height) {
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart');
return chart;
}
function getContainerGroup(chart, margin, width, height) {
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main');
return main;
}
function renderXAxis(x, main, height) {
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
var xAxisElement = main.select('.x.axis');
if (xAxisElement.empty()) {
xAxisElement = main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'x axis')
}
xAxisElement.call(xAxis);
return xAxis;
}
function renderYAxis(y, main) {
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
var yAxisElement = main.select('.y.axis');
if (yAxisElement.empty()) {
yAxisElement = main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'y axis');
}
yAxisElement.call(yAxis);
return yAxis;
}
function renderScatterplot(main, data, scales) {
var g = main.append("svg:g");
var divTooltip = d3.select('.tooltip1');
if (divTooltip.empty()) {
divTooltip = d3.select('body').append('div')
.attr('class', 'tooltip1')
.style('opacity', 0);
}
g.selectAll("scatter-dots")
.data(data, function(d, i) {
return i;
})
.enter().append("svg:circle")
.attr("cx", function(d, i) {
return scales.x(d[0]);
})
.attr("cy", function(d) {
return scales.y(d[1]);
})
.on('click', function(d) {
// log(d.toString());
})
.attr("r", 8);
}
function addZoomRect(main, scales, zoom) {
var zoomRect = main.append('rect')
.attr('width', function() {
return scales.x(d3.max(scales.x.domain()));
})
.attr('height', function() {
return scales.y(d3.min(scales.y.domain()));
})
.attr('x', 0)
.attr('y', 0)
.attr('fill', 'transparent')
.attr('stroke', 'red');
if (zoom) {
zoomRect.call(zoom);
}
return zoomRect;
}
function restrictYPanning(zoom) {
var zoomTranslate = this.translate();
this.translate([zoomTranslate[0], 0]);
}
function addXScrollEndEvent(scales, direction, data) {
var zoomTranslate = this.translate();
var condition;
var currentDomainMax = d3.max(scales.x.domain());
var dataMax = d3.max(data, function(d) {
return d[0];
});
var currentDomainMin = d3.min(scales.x.domain());
var dataMin =
d3.min(data, function(d) {
return d[0];
});
if (currentDomainMax > dataMax && direction === 'right') {
//log('currentDomainMax ', currentDomainMax);
//log('dataMax ', dataMax);
//log('----------------');
condition = true;
}
if (dataMin > currentDomainMin && direction === 'left') {
//log('currentDomainMin ', currentDomainMin);
//log('dataMin ', dataMin);
//log('----------------');
condition = true;
}
//var xRightLimit, xTranslate;
//if (direction === 'right') {
// xRightLimit = scales.x(d3.max(scales.x.domain())) - (getMargin().width + 60);
//
// xTranslate = 0 - zoomTranslate[0];// + scales.x(d3.min(scales.x.domain()));
//
// condition = xTranslate > xRightLimit;
//} else {
// xRightLimit = scales.x(d3.min(scales.x.domain()));
//
// xTranslate = zoomTranslate[0];// + scales.x(d3.min(scales.x.domain()));
//
// condition = xTranslate > xRightLimit;
//}
return condition;
}
function onZoom(zoom, main, xAxis, yAxis, scales, data) {
//var xAxis = d3.svg.axis()
// .scale(scales.x)
// .orient('bottom');
//var yAxis = d3.svg.axis()
// .scale(scales.y)
// .orient('left');
//alert(data);
var translate = zoom.translate();
var direction = '';
if (translate[0] < ns.lastTranslate[0]) {
direction = 'right';
} else {
direction = 'left';
}
ns.lastTranslate = translate; //d3.transform(main.attr('transform')).translate ;
// log('zoom translate', ns.lastTranslate);
// log('d3 Event translate', d3.event.translate);
window.scales = scales;
window.data = data;
// ns.lastTranslate = translate;
var divTooltip = d3.select('.tooltip1');
if (divTooltip.empty()) {
divTooltip = d3.select('body').append('div')
.attr('class', 'tooltip1')
.style('opacity', 0);
}
restrictYPanning.call(zoom);
var xScrollEndCondition = addXScrollEndEvent.call(zoom, scales, direction, data);
if (xScrollEndCondition) {
if (zoom.onXScrollEnd) {
zoom.onXScrollEnd.call(this, {
'translate': translate,
'direction': direction
});
}
}
main.select(".x.axis").call(xAxis);
main.select(".y.axis").call(yAxis);
var dataElements = main.selectAll("circle")
.data(data, function(d, i) {
return i;
});
dataElements.attr("cx", function(d, i) {
return scales.x(d[0]);
})
.attr("cy", function(d) {
return scales.y(d[1]);
}).attr("r", 8);
dataElements.enter().append("svg:circle")
.attr("cx", function(d, i) {
return scales.x(d[0]);
})
.attr("cy", function(d) {
return scales.y(d[1]);
}).on('click', function(d) {
// log(d.toString());
})
.attr("r", 8);
// log(direction);
}
//var xRangeMax;
//var xRangeMin;
ns.lastTranslate = [0, 0];
/**
* Created by Lenovo on 7/4/2015.
*/
function log(titlee, msgg) {
var msg = msgg;
var title;
if (titlee) {
title = titlee + ':-->';
}
if (!msgg) {
msg = titlee;
title = '';
} else {
if (Array.isArray(msgg)) {
msg = msgg.toString();
}
if ((typeof msg === "object") && (msg !== null)) {
msg = JSON.stringify(msg);
}
}
var tooltip = d3.select('.tooltip1');
var earlierMsg = tooltip.html();
var num = tooltip.attr('data-serial') || 0;
num = parseInt(num) + 1;
msg = '<div style="border-bottom:solid 1px green"><span style="color:white">' + num + ')</span><strong>' + title + '</strong> ' + decodeURIComponent(msg) + ' </div>';
tooltip.html('<br>' + msg + '<br>' + earlierMsg).style({
'color': 'lightGray',
'background': 'darkGray',
'font-family': 'courier',
'opacity': 1,
'max-height': '200px',
'overflow': 'auto'
})
.attr('data-serial', num);
}
function addLoggerDiv() {
var divTooltip = d3.select('.tooltip1');
if (divTooltip.empty()) {
divTooltip = d3.select('body').append('div')
.attr('class', 'tooltip1')
.style({
'opacity': 0,
'position': 'relative'
});
d3.select('body').append('div')
.text('close')
.style({
'top': 0,
'right': 0,
'position': 'absolute',
'background': 'red',
'color': 'white',
'cursor': 'pointer'
})
.on('click', function() {
var thisItem = divTooltip;
var txt = thisItem.text();
var display = 'none';
if (txt === 'close') {
thisItem.text('open');
display = 'none';
} else {
thisItem.text('close');
display = 'block';
}
devTooltip.style('display', display);
});
d3.select('body').append('div')
.text('clear')
.style({
'top': 0,
'right': 20,
'position': 'absolute',
'background': 'red',
'color': 'white',
'cursor': 'pointer'
})
.on('click', function() {
divTooltip.html('');
divTooltip.attr('data-serial', '0');
});
}
}
$(document).ready(function() {
var data = getData();
var __ret = getMargin();
var margin = __ret.margin;
var width = __ret.width;
var height = __ret.height;
var scales = {};
var xRangeMax = width;
scales.x = defineXScale(data, [], [0, xRangeMax]);
scales.y = defineYScale(data, [], [height, 0]);
addLoggerDiv();
var svg = getSvg(width, margin, height);
var main = getContainerGroup(svg, margin, width, height);
// draw the x axis
var xAxis = renderXAxis(scales.x, main, height);
// draw the y axis
var yAxis = renderYAxis(scales.y, main);
var thisobj = this;
var zoom = d3.behavior.zoom().x(scales.x).y(scales.y).scaleExtent([1, 1]).on('zoom', function() {
onZoom.call(null, zoom, main, xAxis, yAxis, scales, data);
});
zoom.onXScrollEnd = function(e) {
var maxX = d3.max(data, function(d) {
return d[0];
});
var minX = d3.min(data, function(d) {
return d[0];
});
var incrementX = Math.floor((Math.random() * 3) + 1);
var maxY = d3.max(data, function(d) {
return d[1];
})
var minY = d3.min(data, function(d) {
return d[1];
})
var incrementY = Math.floor((Math.random() * 1) + 16);
var xRangeMin1, xRangeMax1, dataPoint;
if (e.direction === 'left') {
incrementX = incrementX * -1;
dataPoint = minX + incrementX;
// log('dataPoint ', dataPoint);
//xRangeMin1 = d3.min(scales.x.range()) - Math.abs(scales.x(minX) - scales.x(dataPoint));
xRangeMin1 = scales.x(dataPoint);
xRangeMax1 = d3.max(scales.x.range());
} else {
dataPoint = maxX + incrementX;
// log('dataPoint ', dataPoint);
//xRangeMax1 = d3.max(scales.x.range()) + (scales.x(dataPoint) - scales.x(maxX));
xRangeMax1 = d3.max(scales.x.range()) + 20; //scales.x(dataPoint);
xRangeMin1 = d3.min(scales.x.range()) //e.translate[0];
}
data.push([dataPoint, incrementY]);
//scales = defineScales(data, width + incrementX, height );
// scales.x = defineXScale(data, [], [xRangeMin1, xRangeMax1]);
// scales.y = defineYScale(data, [], [height, 0]);
scales.x.domain(d3.extent(data, function(d) {
return d[0];
}));
x = scales.x;
y = scales.y;
xAxis = renderXAxis(scales.x, main, height);
// draw the y axis
yAxis = renderYAxis(scales.y, main);
zoom.x(scales.x).y(scales.y);
}
var zoomRect = addZoomRect(main, scales, zoom);
renderScatterplot(main, data, scales);
});
})(window.chartBuilder);
/* Styles go here */
.chart {
font-family: Arial, sans-serif;
font-size: 10px;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.bar {
fill: steelblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have created zoom.onXScrollEnd function to add new points to data.
Hope it helps.