Behroz Sikander Behroz Sikander - 22 days ago 11
Javascript Question

Fixed center with dynamic axis scaling

EDIT:
I have edited my code. Now you can directly run the code inside stackoverflow.

Last day, I started with D3 and having some problems in fixing the dynamic axis. I am new to D3 :)

I am trying to build the following.
enter image description here

I am trying to plot some 2d streaming data with D3. Data comes in through web sockets. So, as the data comes in I plot the new points. I have been somewhat succesful in that but my axis are not behaving as expected.

As the new data comes in, I rescale my x and y axis. Forexample if (20,100) comes in the following plot then I will scale my y-axis to accomadate 100. But if you look closely the point where x and y axis meet is not on 0. Initially it was on (0,0) but after rescaling it went way below. How can I fix the center a (0,0) while dynamically rescaling my axis ?

Can you please help me. Here is my code.



<!DOCTYPE html>
<meta charset="utf-8">
<style>
.line {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}

div.bar {
display: inline-block;
width: 20px;
height: 75px; /* We'll override this later */
background-color: teal;
}


/* Format X and Y Axis */
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}

.axis text {
font-family: sans-serif;
font-size: 11px;
}

</style>
<svg width="960" height="500"></svg>
<br/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>

function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

var dataset = []; // Initialize empty array
var numDataPoints = 15; // Number of dummy data points

for(var i=0; i<numDataPoints; i++) {

dataset.push([getRandomInt(-80,80), getRandomInt(-60,60)]); // Add new number to array
}

var width = 1020;
var height = 840;

var xScale = d3.scale.linear().domain([d3.min(dataset, function(d) { return d[0]; } ), d3.max(dataset, function(d) { return d[0]; })])
.range([20, width - 20 * 2])
.nice();
var yScale = d3.scale.linear().domain([d3.min(dataset, function(d) { return d[1]; } ), d3.max(dataset, function(d) { return d[1]; })])
.range([height - 20, 20])
.nice();

var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yAxis = d3.svg.axis().scale(yScale).orient("left");

var svg = d3.select("svg")
.attr("height",height)
.attr("width", width)
.append("g")
.attr("transform","translate(20,20)")
.attr("width", width- 40)
.attr("height", height- 40);



// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
//.attr("transform", "translate(0," + (height - 20) + ")")
//.style("position", "fixed")
.call(xAxis);

//y-axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + xScale(0) + ",0)")
//.attr("transform", "translate(" + 20 + ",0)")
//.style("position", "fixed")
.call(yAxis);

svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle") // Add circle svg
.attr("cx", function(d) {
return xScale(d[0]); // Circle's X
})
.attr("cy", function(d) { // Circle's Y
return yScale(d[1]);
})
.attr("r", 2)
.call(updatePlotWithSocketData);

function updatePlotWithSocketData() {
console.log(dataset.length);

for(var i=0; i<numDataPoints; i++) {
dataset.push([getRandomInt(-200,200), getRandomInt(-100,100)]); // Add new number to array
}

// Update scale domains
xScale.domain([d3.min(dataset, function(d) { return d[0]; } ), d3.max(dataset, function(d) { return d[0]; })]);
yScale.domain([d3.min(dataset, function(d) { return d[1]; } ), d3.max(dataset, function(d) { return d[1]; })]);
xScale.range([20, width - 20 * 2]).nice();
yScale.range([height - 20, 20]).nice();

// Update old points to the new scale
svg.selectAll("circle")
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d[0]); // Circle's X
})
.attr("cy", function(d) {
return yScale(d[1]); // Circle's Y
});

// Update circles
svg.selectAll("circle")
.data(dataset) // Update with new data
.enter()
.append("circle")
.transition() // Transition from old to new
.duration(1000) // Length of animation
.each("start", function() { // Start animation
d3.select(this) // 'this' means the current element
.attr("fill", "red") // Change color
.attr("r", 5); // Change size
})
.delay(function(d, i) {
return i / dataset.length * 500; // Dynamic delay (i.e. each item delays a little longer)
})

.attr("cx", function(d) {
return xScale(d[0]); // Circle's X
})
.attr("cy", function(d) {
return yScale(d[1]); // Circle's Y
})
.each("end", function() { // End animation
d3.select(this) // 'this' means the current element
.transition()
//.duration(500)
.attr("fill", "black") // Change color
.attr("r", 2); // Change radius
});



/* force.on("tick", function() {
nodes[0].x = width / 2;
nodes[1].y = height / 2;
}); */

/* var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yAxis = d3.svg.axis().scale(yScale).orient("left"); */


svg.selectAll(".x.axis")
.transition()
.duration(1000)
.call(xAxis);

// Update Y Axis
svg.selectAll(".y.axis")
.transition()
.duration(1000)
.call(yAxis);

setTimeout(updatePlotWithSocketData, 3000);

}


// Socker code
/*
var socket;
var registered = false;

function startDataReceptionFromSocket() {
console.log("opening socket");
//on http server use document.domain instead od "localhost"
//Start the websocket client
socket = new WebSocket("ws://localhost:8887/");

//When the connection is opened, login.
socket.onopen = function() {
console.log("Opened socket.");
//register the user

};

socket.onmessage = function(a) {
//process the message here
console.log("received message socker => : " + a.data);
var message = JSON.parse(a.data);
console.log("message =>" + message.message);
var numbers = message.message.split(" ")
dataset.push([numbers[0],numbers[1]]);
updatePlotWithSocketData();
}

socket.onclose = function() { console.log("Closed socket."); };
socket.onerror = function() { console.log("Error during transfer."); };
}

// On document load, open the socket connection
(function() {
startDataReceptionFromSocket();
})();*/



</script>




Answer

It seems to me that the solution is setting the translate again when you update the axes.

Right now, in your code, the domains (for the x and y scales) are changing, but the axes are translated using the original xScale(0) and yScale(0) positions. This demo reproduces the problem:

var margin = 10;
var width = 250, height = 250;

var svg = d3.select("#mydiv")
	.append("svg")
	.attr("width", width)
	.attr("height", height);
	
var xScale = d3.scale.linear()
	.range([margin, width - margin])
	.domain([-10, 10]);
	
var yScale = d3.scale.linear()
	.range([margin, height - margin])
	.domain([-10, 10]);
	
var xAxis = d3.svg.axis()
	.scale(xScale)
	.orient("bottom");
	
var yAxis = d3.svg.axis()
	.scale(yScale)
	.orient("left");
	
svg.append("g").attr("class", "x axis")
	.attr("transform", "translate(0," + yScale(0) + ")")
	.call(xAxis);
	
svg.append("g").attr("class", "y axis")
	.attr("transform", "translate(" + xScale(0) + ",0)")
	.call(yAxis);
	
d3.select("#btn").on("click", randomize);

function randomize(){
	xScale.domain([Math.floor(Math.random()*50)*(-1),Math.floor(Math.random()*50)]);
	yScale.domain([Math.floor(Math.random()*50)*(-1),Math.floor(Math.random()*50)]);
	
	 svg.selectAll(".x.axis")
	        .transition()
        	.call(xAxis);

	    svg.selectAll(".y.axis")
	        .transition()
	        .call(yAxis);
	
}
.axis path, .axis line{
	fill: none;
	stroke: black;
    shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<button id="btn">Randomize</button>
<div id="mydiv"></div>

Now the solution. The next demo has the same code, but updating the positions like this:

svg.selectAll(".x.axis")
    .transition()
    .duration(1000)
    .attr("transform", "translate(0," + yScale(0) + ")")
    .call(xAxis);

svg.selectAll(".y.axis")
    .transition()
    .duration(1000)
    .attr("transform", "translate(" + xScale(0) + ",0)")
    .call(yAxis);

Check the demo:

var margin = 10;

var width = 250, height = 250;

var svg = d3.select("#mydiv")
	.append("svg")
	.attr("width", width)
	.attr("height", height);
	
var xScale = d3.scale.linear()
	.range([margin, width - margin])
	.domain([-10, 10]);
	
var yScale = d3.scale.linear()
	.range([margin, height - margin])
	.domain([-10, 10]);
	
var xAxis = d3.svg.axis()
	.scale(xScale)
	.orient("bottom");
	
var yAxis = d3.svg.axis()
	.scale(yScale)
	.orient("left");
	
svg.append("g").attr("class", "x axis")
	.attr("transform", "translate(0," + yScale(0) + ")")
	.call(xAxis);
	
svg.append("g").attr("class", "y axis")
	.attr("transform", "translate(" + xScale(0) + ",0)")
	.call(yAxis);
	
d3.select("#btn").on("click", randomize);

function randomize(){
	xScale.domain([Math.floor(Math.random()*50)*(-1),Math.floor(Math.random()*50)]);
	yScale.domain([Math.floor(Math.random()*50)*(-1),Math.floor(Math.random()*50)]);
	
	 svg.selectAll(".x.axis")
	        .transition()
    	    .duration(1000)
					.attr("transform", "translate(0," + yScale(0) + ")")
        	.call(xAxis);

	    svg.selectAll(".y.axis")
	        .transition()
	        .duration(1000)
					.attr("transform", "translate(" + xScale(0) + ",0)")
	        .call(yAxis);
	
}
.axis path, .axis line{
	fill: none;
	stroke: black;
    shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<button id="btn">Randomize</button>
<div id="mydiv"></div>

Comments