D3.js : Set multiple attributes of SVG shape - d3.js

My data array looks like this:
circleData = {
"x": [10,20,30,40], "r":[1,2,3,4], "y": [0,0,0,0]
},
{
"x": [15,25,35,45], "r":[5,6,7,8], "y": [20,20,20,20]
}
I want to create two rows of four circles with the above x, r, and y parameters. But how? The problem is, if the x values are bound in order to create the circles, then the circles have "access" to the x data, but no longer to the r or y data.

As mentioned in Lars Kotthoff's comment, the easiest way will probably be to restructure your data. Because D3's data binding relies on arrays, one approach would be something like this:
circleData = [{
"x": [10,20,30,40], "r":[1,2,3,4], "y": [0,0,0,0]
},
{
"x": [15,25,35,45], "r":[5,6,7,8], "y": [20,20,20,20]
}];
var circles = d3.merge(circleData.map(function(d) {
return d3.zip(d.x, d.y, d.r);
}));
This will give you a 2-dimensional array containing 8 arrays representing all 8 circles with their cx, cy, and r parameters.
You may then bind circles as your data and operate very D3-ish on it.
var circleData = [{
"x": [10, 20, 30, 40],
"r": [1, 2, 3, 4],
"y": [0, 0, 0, 0]
}, {
"x": [15, 25, 35, 45],
"r": [5, 6, 7, 8],
"y": [20, 20, 20, 20]
}];
var circles = d3.merge(circleData.map(function(d) {
return d3.zip(d.x, d.y, d.r);
}));
var svg = d3.select("body")
.append("svg").append("g")
.attr("transform", "translate(50,50)"); // Translate for the sake of visibility
svg.selectAll("circle")
.data(circles).enter()
.append("circle")
.attr({
"cx": function(d) {
return d[0];
},
"cy": function(d) {
return d[1];
},
"r": function(d) {
return d[2];
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Related

D3 updating data on parent element wont reflect on child elements

Let's consider I need to have a bunch of circles wrapped in <g> elements and I've bound my data to this parents.(initialNodes)
when I press the update button, the update function takes in the new data (newNodes) and I expect the x/y positions of circles to get updated, but as you can see in console, despite the fact that d.x/d.y is correctly printed, it's not taken into account in return d.x and therefore just Enter group is added to canvas.
What am I doing wrong? How can I reflect the updated data on parent <g>, on child elements?
var color = d3.schemeCategory10;
var initialNodes = [
{"id": 0, "x": 50, "y": 50},
{"id": 1, "x": 100, "y": 100},
];
var vis = d3.select("body").append("svg").attr("width", 200).attr("height", 200);
update(initialNodes);
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var circlesGroup = vis.selectAll("g.stop").data(data, function(d){return d.id});
var circlesEnter = circlesGroup.enter().append("g").attr("class", "stop");
var circlesExit = circlesGroup.exit().remove();
// ENTER
// Create new elements as needed.
circlesEnter
.append("circle")
.attr("r", 15)
.transition().duration(750)
.attr("cx", function (d) {
console.log('ENTERING: id:'+d.id+' position:'+d.x+','+d.y);
return d.x;
})
.attr("cy", function (d) {return d.y;})
.style("fill", 'red');
// UPDATE
// Update old elements as needed.
circlesGroup
.transition().duration(750)
.attr("cx", function (d) {
console.log('ENTERING: id:'+d.id+' position:'+d.x+','+d.y);
return d.x;})
.attr("cy", function (d) {return d.y;});
// EXIT
// Remove old elements as needed.
circlesExit
.remove();
}
var newNodes = [
{"id": 0, "x": 50, "y": 100},
{"id": 1, "x": 50, "y": 30},
{"id": 2, "x": 100, "y": 50}
];
var updateNodes = function() {
update(newNodes);
}
// Add the onclick callback to the button
d3.select("#updatebutton").on("click", updateNodes);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button id="updatebutton">Update</button>
circleGroup is a selection of g elements. It has no purpose to set cx and cy for a g element.
After creating new nodes select all g.stop and then the containing circle. Why? Read the d3-selection doc.
There is no reason to add the second remove().
var color = d3.schemeCategory10;
var initialNodes = [
{"id": 0, "x": 50, "y": 50},
{"id": 1, "x": 100, "y": 100},
];
var vis = d3.select("body").append("svg").attr("width", 200).attr("height", 200);
update(initialNodes);
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var circlesGroup = vis.selectAll("g.stop").data(data, function(d){return d.id});
var circlesEnter = circlesGroup.enter().append("g").attr("class", "stop");
var circlesExit = circlesGroup.exit().remove();
// ENTER
// Create new elements as needed.
circlesEnter
.append("circle")
.attr("r", 15)
.style("fill", 'red');
vis.selectAll("g.stop").select("circle")
.transition().duration(750)
.attr("cx", function (d) {
console.log('ENTERING2: id:'+d.id+' position:'+d.x+','+d.y);
return d.x;})
.attr("cy", function (d) {return d.y;});
}
var newNodes = [
{"id": 0, "x": 50, "y": 100},
{"id": 1, "x": 50, "y": 30},
{"id": 2, "x": 100, "y": 50}
];
var updateNodes = function() {
update(newNodes);
}
// Add the onclick callback to the button
d3.select("#updatebutton").on("click", updateNodes);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div><button id="updatebutton">Update</button></div>

Can I move one data series down to a lower x axis, programmatically?

I have a d3 stacked column chart that I'm very happy with. The full code is in a JS Fiddle.
What I'd like to do is lop the last data series off, and set it on its own axis, but ensure that it maintains the same scale. So if this is my data:
var dataset = [
// apples
[{"x": 1, "y": 5 }, { "x": 2, "y": 4 }, { "x": 3, "y": 2 }, { "x": 4, "y": 7 }, { "x": 5, "y": 23 }],
// oranges
[{ "x": 1, "y": 10 }, { "x": 2, "y": 12 }, { "x": 3, "y": 19 }, { "x": 4, "y": 23 }, { "x": 5, "y": 17 }],
// grapes
[{ "x": 1, "y": 22 }, { "x": 2, "y": 28 }, { "x": 3, "y": 32 }, { "x": 4, "y": 35 }, { "x": 5, "y": 43 }],
// carrots
[{"x": 1, "y": 5 }, { "x": 2, "y": 4 }, { "x": 3, "y": 23 }, { "x": 4, "y": 2 }, { "x": 5, "y": 7 }]
];
I'd like to keep apples, oranges and grapes stacked, but I want carrots separated out. Carrots is always the last series. I was hoping I could draw the carrots into the same SVG with this:
var lower_svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", b);
var lower_rects = lower_svg.selectAll("rect")
.data(dataset[3])
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", h)
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand());
But a) that doesn't work (it doesn't draw anything) and b) that calls on the data series 3, which happens to be the last one in this example but isn't always.
And ... if it did work it would draw the carrots twice, once stacked with the other fruits and once below. I only want to draw it once, below.
What I want is to have this chart of various fruit: https://jsfiddle.net/oa7hho9q/17/
And this chart of carrots: https://jsfiddle.net/oa7hho9q/19/
Using the same x and y scales and pulling from the same dataset, where, carrots is just the last series in the set.
I have addressed your problem like this:
Step 1:
I pop out the carrot related data.
var carrots = dataset.pop(); store it in variable carrots
Step 2
I make 2 groups
//this g(group) will hold the stacked chart for carrot
var svgcarrot = svg.append("g").attr("transform", "translate(0,200)");
//this g(group) will hold the stacked chart for other fruits
var svg = svg.append("g").attr("transform", "translate(0,-150)");
//you may change the translate to move the chart as per your choice of positioning.
Step3
Make a function to make charts input svg group and its related dataset
//here svg is the group on which you wish to draw the chart.
//dataset is the data for which the chart need to be drawn.
function makeChart(dataset, svg) {
Step4
Inside your makeChart function your usual stack bar chart code.
function makeChart(dataset, svg) {
var stack = d3.layout.stack();
stack(dataset);//set data
xScale = d3.scale.ordinal()
.domain(d3.range(dataset[0].length))
.rangeRoundBands([0, w], 0.05);
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 / 2]);//2 chart so height/2
//make groups for fruits
var groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
//make rectangles
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, i) {
return h - b - (yScale(d.y0 + d.y));
})
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand());
}
Step 5
Now make your first chart
makeChart(dataset, svg);//note dataset has no carrot data as its popped in step1 also the svg container group made in step 2
makeChart([carrots], svgcarrot);//make carrot chart note the svgcarrot container group made in step 2
working example here

d3.js path doesn't respect domain and range

There must be something obvious I'm missing here.
I'm trying to draw a simple line and this is my javascript:
// CRASH DATA
var lineData = [
{ "x": 0, "y": 0.5},
{ "x": 2, "y": 0.1},
{ "x": 4, "y": -0.5},
{ "x": 6, "y": -0.8},
{ "x": 8, "y": -0.9},
{ "x": 10, "y": -0.10},
{ "x": 12, "y": -0.10},
{ "x": 14, "y": -0.11},
{ "x": 16, "y": -0.10},
{ "x": 18, "y": -0.9},
{ "x": 20, "y": -0.7},
{ "x": 22, "y": -0.6},
{ "x": 24, "y": -0.5},
{ "x": 26, "y": -0.3},
{ "x": 28, "y": -0.1},
{ "x": 30, "y": 0.2},
{ "x": 32, "y": 0.4},
{ "x": 34, "y": 0.8},
{ "x": 36, "y": 0.8},
{ "x": 38, "y": 0.7},
{ "x": 40, "y": 0.4},
{ "x": 42, "y": 0.4},
{ "x": 44, "y": 0.4},
{ "x": 46, "y": 0.2},
{ "x": 48, "y": 0.1},
{ "x": 50, "y": 0}
];
//DRAW TRAJECTORY
function draw(data){
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
//This is the accessor function we talked about above
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.x; }));
y.domain(d3.extent(data, function(d) { return d.y; }));
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g") // Add the Y Axis
.attr("class", "y axis")
.call(yAxis);
svg.append("path") // Add the lineFunction path.
.attr("class", "line")
.attr("d", lineFunction(data));
};
//PUT EVERYTHING ON SCREEN
$( document ).ready(function() {
draw(lineData);
});
And here is the outcome:
You aren't actually using the scales you define. Your line function should be
var lineFunction = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); })
.interpolate("linear");
Complete example here.

How do I define a value accessor for a stacked bar graph?

I'm trying to create a stack bar graph using the stack layout.
I can make it work only if I pass it an array of x,y coordinates. But I want to be able to add meta data to it, such as series title.
I've read the docs (https://github.com/mbostock/d3/wiki/Stack-Layout), and seen how it's done on a steamgraph (Correct usage of stack.values([accessor]) in D3 Streamgraph?). The problem with these examples is that they don't take into account things like y scale, making it difficult to establish variables such as yStackMax.
I also need the data to be passed to the stack() function early on, because I'm planning to redraw this and other things when the data is refreshed. In short, instead of:
var data = [
[
{ "x": 0, "y": 91},
{ "x": 1, "y": 290}
],
[
{ "x": 0, "y": 9},
{ "x": 1, "y": 49}
],
[
{ "x": 0, "y": 10},
{ "x": 1, "y": 25}
]
];
var layers = d3.layout.stack()(data);
var yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
... which works, I want to be able to do:
var data = [
{
"name": "apples",
"values": [
{ "x": 0, "y": 91},
{ "x": 1, "y": 290}
]
},
{
"name": "oranges",
"values": [
{ "x": 0, "y": 9},
{ "x": 1, "y": 49}
]
},
{
"name": "potatoes",
"values": [
{ "x": 0, "y": 10},
{ "x": 1, "y": 25}
]
}
];
var layers = d3.layout.stack()(data).values(function(d) { return d.values; });
var yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
... which doesn't work.
This is the working code fiddle: http://jsfiddle.net/StephanTual/E6FeP/
This is the fiddle for the code that doesn't work: http://jsfiddle.net/StephanTual/Tnj8W/
Here's an updated fiddle.
The key part was:
// define the accessor before adding in the data
var layers = d3.layout.stack().values(function(d) { return d.values; })(data);
var yStackMax = d3.max(layers, function(layer) { return d3.max(layer.values, function(d) { return d.y0 + d.y; }); });
And then I made a couple other adjustments as necessary to access the .values.

Binding "mouseover" events to points on a line in d3.js

I would like to get the coordinates of a point on a line by clicking on the line using the following code:
var lineData = [ { "x": 1, "y": 5}, { "x": 20, "y": 20},
{ "x": 40, "y": 10}, { "x": 60, "y": 40},
{ "x": 80, "y": 5}, { "x": 100, "y": 60}];
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
var lineGraph = svgContainer.append("path")
.data([lineData]).attr("d", lineFunction)
//.attr("d", lineFunction(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none")
.on('mousedown', function(d) {
console.log({"x":d.x, "y":d.y})
});
(I updated the code to address the comments, but I still get "Object {x: undefined, y: undefined}")
I keep getting an "undefined" when clicking on the line. Am I missing a step?
You can get the coordinates of an event using d3.event:
.on("mousedown", function() {
console.log({"x": d3.event.x, "y": d3.event.y});
});
use mouse event
.on('mousedown', function(d) {
var m = d3.mouse(this);
console.log("x:"+m[0]+" y:"+m[1]);
});
in your function m[0] and m[1] gives you X and Y.

Resources