I'm trying to get the values for x and y to make a circles using d3.js v4. With the following code I manage to create the chart like behavior of the circles, but when I try to run the same code in v4 it doesn't work anymore. I know that there are some differences in the update to v4 but I didn't find any information about it. So i was wondering if someone can help me to run this code in d3.js v4.
Here is the code using v3 (it will break using v4):
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v3.min.js'></script>
This is the expected behaviour, and I've explained this before in this answer (not a duplicate, though).
What happened is that Mike Bostock, D3 creator, introduced a magic behaviour in D3 v2, which he kept in D3 v3.x, but decided to abandon in D3 v4.x. To read more about that, have a look here: What Makes Software Good? This is what he says:
D3 2.0 introduced a change: appending to the enter selection would now copy entering elements into the update selection [...] D3 4.0 removes the magic of enter.append. (In fact, D3 4.0 removes the distinction between enter and normal selections entirely: there is now only one class of selection.)
Let's see it.
Here is your code with D3 v3:
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v3.min.js'></script>
Now the same code, with D3 v4. It will "break":
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v4.min.js'></script>
By "break" I mean the circles will be appended, but they will not receive the x and y properties in the "enter" selection, and they will default to zero. That's why you see all circles at the top left corner.
Solution: merge the selections:
circles.enter().append('circle')
.attr('r', 10)
.merge(circles) //from now on, enter + update
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
According to the API, merge()...
... is commonly used to merge the enter and update selections after a data-join. After modifying the entering and updating elements separately, you can merge the two selections and perform operations on both without duplicate code.
Here is the code with merge():
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10)
.merge(circles) //from now on, enter + update
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v4.min.js'></script>
Update pattern was changed in d3v4.
Excerpt from the documentation:
In addition, selection.append no longer merges entering nodes into the
update selection; use selection.merge to combine enter and update
after a data join.
You should rewrite your code this way:
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data){
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10).merge(circles) // <== !!!
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
//Exit
circles.exit().remove();
}
var myObjects = [
{x: 100, y: 100},
{x: 130, y: 120},
{x: 80, y: 180},
{x: 180, y: 80},
{x: 180, y: 40}
];
render(myObjects);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
Related
I want to label each group is there a better way to do it?
Right now I am doing this:
const svgViewport = d3.select("body")
.append("svg")
.attr("width", 150)
.attr("height", 150);
let myData = [
[{
x: 30,
y: 40
},
{
x: 30,
y: 60
}
],
[{
x: 70,
y: 40
},
{
x: 70,
y: 60
}
]
];
let labelData=[{name:"group1",x:20,y:30},
{name:"group2",x:70,y:30}];
const groups = svgViewport.selectAll(null)
.data(myData)
.enter()
.append("g");
const circles = groups.selectAll(null)
.data(d => d)
.enter()
.append("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.attr("r", 10);
const labels=svgViewport.selectAll("g").data(labelData).append("text").text((d)=>{return d.name;}).attr("x",(d)=>{return d.x})
.attr("y",(d)=>{return d.y});
I want to label each group of shapes can this be done without holding in the labelData x and y positions?
I am working on a D3 project and I am wondering how can I draw lines between two points by reading the data from a JSON document. Document provides 4 Xs and 4 Ys values, I couldn't find a way to do it efficiently and everything I tried just got me errors(code doesn't work). Because it is 15000+ lines and it is impossible me to write the coordinates one by one also it is not a way to create a dynamic program/software.
Thanks in advance !
Create your container :
var svg = d3.select('#container').append('svg')
.attr('width', 500)
.attr('height', 500)
Your data :
var thisData = {
x1: 50,
y1: 50,
x2: 450,
y2: 450
};
Create your link using the data above (or your own) :
var link = svg.selectAll(".link")
.data([thisData]) //data above
.enter().append("line")
.attr("class", "link")
.style('stroke','black')
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
Position links :
link.attr("x1", function(d) { console.log(d); return d.x1; })
.attr("y1", function(d) { return d.y1; })
.attr("x2", function(d) { return d.x2; })
.attr("y2", function(d) { return d.y2; });
All this needs is two points, [X1,Y1] & [X2,Y2] or in your case 15000 links. D3 will handle it all for you :))
var svg = d3.select('#container').append('svg')
.attr('width', 500)
.attr('height', 500)
//.append('g')
var thisData = {
x1: 50,
y1: 50,
x2: 450,
y2: 450
};
var link = svg.selectAll(".link")
.data([thisData])
.enter().append("line")
.attr("class", "link")
.style('stroke','black')
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
//console.log(line);
link.attr("x1", function(d) { console.log(d); return d.x1; })
.attr("y1", function(d) { return d.y1; })
.attr("x2", function(d) { return d.x2; })
.attr("y2", function(d) { return d.y2; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id='container'>
</div>
As long as your data looks like the above data or a set of points like so :
var thisData = { //first line
x1: 50,
y1: 50,
x2: 450,
y2: 450
},
{ //second
x1: 50,
y1: 50,
x2: 450,
y2: 450
},
{//third
x1: 50,
y1: 50,
x2: 450,
y2: 450
};
EDIT
Here is how you would show your data.
Updated fiddle : https://jsfiddle.net/reko91/odcyduvf/1/
I have set attached your data to a variable - 'thisData'.
Now to just draw the links you want to pass the links to D3 like above, like this :
.data(thisData.links)
Im not sure why you have x1,x2,x3,x4. If this is a path, for example, the points would be x1,y1 : x2,y2 : x3,y3 and so on then you can need to format your code so each individual link has x1,y1,x2,y2.
I would have shown all your data, but JSFiddle cant handle 135,000 lines of data :P
Thanks in advance for any help.
I'm new to D3 and javascript as a whole. I've been pretty stuck on this for a while now even searching through other similar posts.
I'd like to flip my stacked bar chart appropriately so that it aligns to the bottom of the SVG.
When I do try it the way I think it should be done, I get a "invalid negative value for '' message.
var dataset = [
[
{ x: 0, y: 5 },
{ x: 1, y: 4 },
{ x: 2, y: 2 },
{ x: 3, y: 7 },
{ x: 4, y: 23 }
],
[
{ x: 0, y: 10 },
{ x: 1, y: 12 },
{ x: 2, y: 19 },
{ x: 3, y: 23 },
{ x: 4, y: 17 }
],
[
{ x: 0, y: 22 },
{ x: 1, y: 28 },
{ x: 2, y: 32 },
{ x: 3, y: 35 },
{ x: 4, y: 43 }
]
];
//Width and Height
var w = 500;
h = 300;
//Create SVG canvas
var svg = d3.select('body').append('svg')
.attr('width', w)
.attr('height', h);
//Set up Stack
var stack = d3.layout.stack();
//Stack dataset
stack(dataset);
//Create scales
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset[0].length))
.rangeRoundBands([0,w], 0.05);
var yScale = d3.scale.linear()
.domain([0,
d3.max(dataset, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y;
});
})
])
.range([0, h]);
//Create colors for scale
var colors = d3.scale.category10();
//Create a Group for each row of data
var groups = svg.selectAll('g')
.data(dataset)
.enter() //only creates placeholder
.append('g') //creates group
.style('fill', function(d, i) {
return colors(i);
});
//Add a rectangle for each datavalue
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) { return yScale(d.y0 + d.y); })
.attr("height", function(d) { return yScale(d.y0) - yScale(d.y0 + d.y); })
.attr("width", xScale.rangeBand());
I think you need to change the way height is getting calculated for each bar. See this plnkr. Is this something you were looking for. May be you can use height (h) variable for your calculations. I changed y parameters like below.
//Add a rectangle for each datavalue
var rects = groups.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
/* .attr("y", function(d) { return yScale(d.y0 + d.y); })
.attr("height", function(d) { return yScale(d.y0) - yScale(d.y0 + d.y); })
.attr("y", function(d) { return yScale(d.y0 ); })
.attr("height", function(d) { return yScale(d.y0 + d.y);})
*/
.attr("y", function(d) {
return yScale(d.y0 + d.y);
})
.attr("height", function(d) {
return h - yScale(d.y0 + d.y);
})
.attr("width", xScale.rangeBand());
I draw a circle and want to run it transition from first to the last point of data set. But can't understand how to do it. Code available here. How can i do it? What is the best practice for this kind of animation?
var data = [[{
x: 10,
y: 10,
r: 10,
color: "red"
}, {
x: 70,
y: 70,
r: 15,
color: "green"
}, {
x: 130,
y: 130,
r: 20,
color: "blue"
}]];
function setUp() {
this.attr("cx", function(d, i) {
return d[i].x;
}).attr("cy", function(d, i) {
return d[i].y;
}).attr("r", function(d, i) {
return d[i].r;
}).attr("fill", function(d, i) {
return d[i].color;
});
}
var canvas = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
canvas.append("rect")
.attr("width", 300)
.attr("height", 300)
.attr("fill", "lightblue");
var circles = canvas.selectAll("circle")
.data(data)
.enter()
.append("circle")
.call(setUp);
Are you looking to do something like this?
var data = [[{
x: 10,
y: 10,
r: 10,
color: "red"
}], [{
x: 70,
y: 70,
r: 15,
color: "green"
}], [{
x: 130,
y: 130,
r: 20,
color: "blue"
}]];
...
var circles = canvas.selectAll("circle")
.data(data[0]);
circles
.enter()
.append("circle")
.call(setUp);
circles
.data(data[1])
.transition()
.duration(2000)
.call(setUp)
.each("end",function(){
circles
.data(data[2])
.transition()
.duration(2000)
.call(setUp);
});
Edits For Comment
If you have a variable number of points, this is a great place to use a recursive function:
// first point
var circles = canvas.selectAll("circle")
.data([data[0]]);
circles
.enter()
.append("circle")
.call(setUp);
// rest of points...
var pnt = 1;
// kick off recursion
doTransition();
function doTransition(){
circles
.data([data[pnt]])
.transition()
.duration(2000)
.call(setUp)
.each("end",function(){
pnt++;
if (pnt >= data.length){
return;
}
doTransition();
});
}
Updated example.
I am trying to write a reusable linechart here, but I am getting the following error on load:
Uncaught TypeError: Cannot read property 'length' of undefined
JSfiddle here: http://jsfiddle.net/2qs95/
I first wrote it "directly" and un-reusable, and that works, but when I tried to convert it to a function it broke for some reason. I am sure it is some scope problem. Just can't seem to figure it out.
Working JSfiddle here: http://jsfiddle.net/ht9rG/
function LineChart (data, chart) {
this.data = data;
this.margin = chart.margin || { top: 0, bottom: 0, left: 0, right: 0 };
this.height = chart.height || 100;
this.width = chart.width || 400;
this.target = chart.target || 'body';
this.yLabelClass = chart.classes.yLabelClass || 'y-label';
this.yTicksClass = chart.classes.yTicksClass || 'y-ticks';
this.specialTextClass = chart.classes.specialTextClass || 'special-text';
}
LineChart.prototype = {
draw: function () {
this.g.selectAll("." + this.yLabelClass)
.data(this.y.ticks(2))
.enter().append("svg:text")
.attr("class", this.yLabelClass)
.text(String)
.attr("x", d3.select(this.target).attr('width') - 50)
.attr("y", function(d) { return -1 * this.y(d) })
.attr("text-anchor", "left")
.attr("dy", 5);
this.g.selectAll("." + this.yTicksClass)
.data(y.ticks(2))
.enter().append("svg:line")
.attr("class", this.yTicksClass)
.attr("y1", function(d) { return -1 * this.y(d); })
.attr("x1", this.x(-0.3))
.attr("y2", function(d) { return -1 * this.y(d); })
.attr("x2", d3.select('svg').attr('width') - 70);
this.g.append('text')
.attr({
class: this.specialTextClass,
y: -50,
x: 0
})
.text(this.data[this.data.length - 1] + 'ms');
this.g.append("svg:path").attr("d", line(this.data));
},
y: d3.scale.linear()
.domain([0, d3.max(this.data)])
.range([this.margin.bottom, this.height - this.margin.bottom]),
x: d3.scale.linear()
.domain([0, this.data.length])
.range([this.margin.left, this.width - this.margin.left]),
svg: d3.select(this.target)
.append("svg:svg")
.attr("width", this.width)
.attr("height", this.height),
g: svg.append("svg:g")
.attr("transform", "translate(0, " + h + ")"),
line: d3.svg.line()
.interpolate('linear')
.x(function (d, i) { return x(i); })
.y(function (d) { return -1 * y(d); })
}
var data = [432, 123, 432,123,765,234,234,656,800,123,431,543,652, 100, 200];
var options = {
width: 800,
height: 100,
margin: {
top: 10,
bottom: 10,
left: 10,
right: 10
},
target: 'body'
};
var x = new LineChart(data, options);
x.draw();
You were missing a few things here and there, mostly
variables you hadn't prefixed with this.,
variables that you had given different names,
use of this inside a D3 callback, at which point it referred to the SVG element and not the chart object,
and a few other things.
I won't list everything here, but a working example is here and you can compare to your version.