d3 data binding not creating child elements using join - d3.js

I am thoroughly stumped and would love a helping hand from anyone who can kick me in the right direction. I'm trying to create groups g and a single rectangle inside them. The join works great for the g parent but it does not create the children. What am I missing? (I'm thinking in joins!)
I've tried replacing the join with an enter().append('g') to no avail either, so I'm missing something.
Here is a jsfiddle.
var svg = d3.select("div#canvas")
.append("svg")
.attr("width", '100%')
.attr("height", '100%');
let NodeData = [
{
'x': 20,
'y': 20,
'id': 'abc',
'fill': '#D4C2F1'
},
{
'x': 20,
'y': 80,
'id': 'def',
'fill': '#B3D2C5'
},
];
function updateNodes(data) {
var groups = svg.selectAll('g').data(data)
.join('g')
.attr('node-id', function (d) { return d.id; })
.attr('transform', function (d) { return `translate(${d.x}, ${d.y})` });
var rects = groups.selectAll('rect')
.data(function (d) { return d; })
.join('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', 80)
.attr('height', 20)
.attr('stroke', '#666666')
.attr('fill', function (d) { return d.fill; });
}
updateNodes(NodeData);

selection.data() requires an array (or function that returns an array). You are not passing an array to .data() when trying to create the child rects. You are passing an object - an individual item in the original data array, so no elements are entered.
To fix this you can simply use:
var rects = groups.selectAll('rect')
.data(function (d) { return [d]; })
Updated fiddle,
But, this is not the best approach if you are just passing the parent's datum as is to a single child. You don't need to use a nested enter/update/exit cycle, you can just append to the parents:
var groups = svg.selectAll('g').data(data)
.join('g')
.attr('node-id', function (d) { return d.id; })
.attr('transform', function (d) { return `translate(${d.x}, ${d.y})` });
var rects = groups.append("rect")
.attr('x', 0)
.attr('y', 0)
.attr('width', 80)
.attr('height', 20)
.attr('stroke', '#666666')
.attr('fill', function (d) { return d.fill; });
Modified fiddle
The new child elements (rects) inherit the bound data of their parents, as you can see here:
var svg = d3.select("div#canvas")
.append("svg")
.attr("width", '100%')
.attr("height", '100%');
let NodeData = [
{
'x': 20,
'y': 20,
'id': 'abc',
'fill': '#D4C2F1'
},
{
'x': 20,
'y': 80,
'id': 'def',
'fill': '#B3D2C5'
},
];
function updateNodes(data) {
var groups = svg.selectAll('g').data(data)
.join('g')
.attr('node-id', function (d) { return d.id; })
.attr('transform', function (d) { return `translate(${d.x}, ${d.y})` });
var rects = groups.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', 80)
.attr('height', 20)
.attr('stroke', '#666666')
.attr('fill', function (d) { return d.fill; });
rects.each(function(d) {
console.log(d);
})
}
updateNodes(NodeData);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.0/d3.min.js"></script>
<div id="canvas" style="border: 1px solid #D9D9D9; width: 100%; height: 600px; margin-top: 6px"></div>

Related

How do I group d3 elements generated from external data?

I have lines and labels that are both being generated from an external data source. What I want is for each iteration of these to be put in a group, so the code looks something like this:
<g>
<text>Text</text>
<line></line>
</g>
<g>
<text>Text</text>
<line></line>
</g>
<g>
<text>Text</text>
<line></line>
</g>
...
...
...
This is my code now which groups all of the elements in one group element:
var group = svg.append("g");
var labels = group.selectAll('text')
.data(data)
.enter()
.append('text')
.attr("class", "text")
.attr('x',function (d) { return xScale(d['Untitled']) + 50})
.attr('y',function (d) { return yScale(d['Untitled2']) - 31.2})
.style("text-anchor", "start")
.style("text-decoration", "underline")
.style("cursor", "move")
.text(function(d) { return d.name });
var lines = group.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('class', 'line')
.style("stroke", "#000")
.style("stroke-width", .7)
.attr('x1',function (d) { return xScale(d['Untitled'])})
.attr('y1',function (d) { return yScale(d['Untitled2'])})
.attr('x2',function (d) { return xScale(d['Untitled']) + 50})
.attr('y2',function (d) { return yScale(d['Untitled2']) - 30});
You could do something like this:
const data = [
{ id: 1, value: 10, label: 'aaa' },
{ id: 2, value: 20, label: 'bbb' },
{ id: 3, value: 30, label: 'ccc' }
];
const svg = d3.select('body').append('svg')
.attr('width', 500)
.attr('height', 500);
const g = svg.selectAll('g').data(data, (d) => {
return d.id;
});
const groupEnter = g.enter().append('g');
groupEnter
.append('text')
.text((d) => {
return d.label;
})
groupEnter
.append('line', (d) => {
return d.value;
})
Here is a working jsfiddle
Basically, the idea is to take the return selection of g.enter().append('g'); and call append two times, the first one to append the text and the second to append the line.
I hope it helps.

d3.js plot not displaying within R Shiny plot area

First, let me accept that i am new to d3.js. I am fairly accustomed to R. I have been doing plot in d3.js, thanks to the stack overflow community and some may d3.js example. I came across r2d3 as a mode to connect R and d3.js plots and hence have used the same to build a connection.
I have created a plot in d3.js and wanted to connect it with the R Shiny output. I am able to connect the plot to the R Shiny. But the plot is always coming out of the shiny plot area.
This is How my plot looks in the R Shiny area :
My Existing d3 plot in R Shiny
Requesting your suggestions on the following :
How to fix the d3.js plot within the R Shiny Area.
My Ui.R code is as below :
column(d3Output("clplot"),width = 12)
My server code is as below :
output$clplot <-r2d3::renderD3(
r2d3(data = cl_d3(),script="d3/cl_dilip_v1.js", d3_version = "5")
)
The js code is attached as below :
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var jsondata = [{ "promotedprice": 100, "nonpromotedprice": 350, "avgprice": 230, "x_value": 80, "brand": "Brand1" }, { "promotedprice": 99, "nonpromotedprice": 170, "avgprice": 130, "x_value": 140, "brand": "Brand2" }, { "promotedprice": 47, "nonpromotedprice": 147, "avgprice": 80, "x_value": 200, "brand": "Brand3" }, { "promotedprice": 100, "nonpromotedprice": 250, "avgprice": 220, "x_value": 260, "brand": "Brand4" }, { "promotedprice": 99, "nonpromotedprice": 170, "avgprice": 130, "x_value": 320, "brand": "Brand5" }];
// Creating the colour Category
var color = d3.scaleOrdinal(d3.schemeCategory10);
// Creating the 1st Comapartment
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 450);
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Attach the Promoted Price Rectangle
var g = svg.selectAll("rect")
.data(data)
.enter()
.append("g")
.classed('rect', false)
.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTime(d.x_value) + "<br/>" + d.nonpromotedprice);
//.style("left", (d3.event.pageX) + "px")
//.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function (d) {
div.transition()
.duration(0)
.style("opacity", 0);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var accent = d3.scaleOrdinal(d3.schemeAccent);
// Line for the 1st Block
g.append("line") // attach a line
.style("stroke", "#E6EAEE")
.style("stroke-width", 17) // colour the line
.attr("x1", function (d) { return d.x_value; }) // x position of the first end of the line
.attr("y1", function (d) { return d.nonpromotedprice; }) // y position of the first end of the line
.attr("x2", function (d) { return d.x_value; }) // x position of the second end of the line
.attr("y2", function (d) { return d.promotedprice; });
// Promoted Price Rectangle for the 1st Block
g.append("rect")
.attr("width", 24)
.attr("height", 13)
.attr("x", function (d) { return d.x_value - 12; })
.attr("y", function (d) { return d.promotedprice; })
.attr("fill", function (d) { return color(d.x_value); })
// Non Promoted Price Rectangle for the 1st Block
g.append("rect")
.attr("width", 24)
.attr("height", 13)
.attr("x", function (d) { return d.x_value - 12; })
.attr("y", function (d) { return d.nonpromotedprice; })
.attr("fill", function (d) { return color(d.x_value); })
// Average Price Rectangle for the 1st Block
g.append("rect")
.attr("width", 24)
.attr("height", 13)
.attr("x", function (d) { return d.x_value - 12; })
.attr("y", function (d) { return d.avgprice; })
.attr("fill", function (d) { return color(d.x_value); });
// Graph X- Axis and Title Text for 1st svg
var y_scale = d3.scaleLinear()
//.domain([d3.min(function (d) { return d.promotedprice }), d3.max(function (d) { return d.nonpromotedprice; })])
.range([370, 0]);
var y_axis = d3.axisLeft()
.scale(y_scale);
y_scale.domain([0, d3.max(data, function (d) { return d.nonpromotedprice; })]).nice();
g.append("g")
.attr("class", "grid")
.attr("transform", "translate(0, 40)")
.attr("fill", "lightgrey")
.attr("stroke-width", 0.15)
.attr("stroke-opacity", 0.2)
//.attr("shape-rendering", crispEdges)
//stroke-opacity: 0.7;shape-rendering: crispEdges;
.call(y_axis
.tickSize(-420)
.tickFormat(""))
;
// PEPSICO AS-IS BRAND CALL OUT
g.append("rect")
.attr("width", 38)
.attr("height", 20)
.attr("x", function (d) { return d.x_value - 2; })
.attr("y", function (d) { return d.promotedprice; })
.attr("fill", function (d) { return color(d.x_value); })
.attr("transform", "translate(" + -15 + "," + -40 + ")");
g.append("text")
//.classed('rotation', true)
//.attr('x', (d,i)=> xScale(i))
.attr("x", function (d) { return d.x_value - 13; })
.attr("y", function (d) { return d.promotedprice - 28; })
.attr("dy", ".35em")
.text(function (d) { return d.brand; })
.style("font-family", "arial")
.style("font-size", 8)
.attr("stroke-width", 0.15)
.attr("fill", "white");
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).select("rect").attr("y", d.y = d3.event.y);
//.attr("x", d.x = d3.event.x)
}
function dragended(d)
{
d3.select(this).classed("active", false);
}
Try to delete this part of code:
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 450);

Plot multiple lines in a for loop in d3

There are many cases online how to plot couple of lines in d3 if you add svg object only once, such as
svg.selectAll("line")
.data(dataset)
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
This plot create one line. I want to create many different lines in an array smth like
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
for (a_ind=1; a_ind<3; a_ind++){
dataset_a=dataset.filter(function(d) { return (d.a==a_ind)})
svg.selectAll("line")
.data(dataset_a) - //!!! using new dataset in each cycle
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
}
I was told it's impossible. Or maybe there is the way? And also how to access then line from dataset_a if i want to delete it with the click of the mouse?
Well, if you want to plot lines, I suggest that you append...<line>s!
The thing with a D3 enter selection is quite simple: the number of appended elements is the number of objects in the data array that doesn't match any element.
So, you just need a data array with several objects. For instance, let's create 50 of them:
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
And, as in the below demo I'm selecting null, all of them will be in the enter selection. Here is the demo:
var svg = d3.select("svg");
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
var lines = svg.selectAll(null)
.data(data)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function(_, i) {
return color(i)
})
.style("stroke-width", 1);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Finally, a tip: as this is JavaScript you can use for loops anywhere you want. However, do not use for loops to append elements in a D3 code. It's unnecessary and not idiomatic.
That being said, whoever told you that it is impossible was wrong, it's clearly possible. Here is a demo (but don't do that, it's a very cumbersome and ugly code):
var svg = d3.select("svg");
var data = d3.range(50).map(function(d, i) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
id: "id" + i
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
for (var i = 0; i < data.length; i++) {
var filteredData = data.filter(function(d) {
return d.id === "id" + i
});
var lines = svg.selectAll(null)
.data(filteredData)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function() {
return color(i)
})
.style("stroke-width", 1);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I would do something like this. Make each data set (1 data set per line), an array inside the final data array .enter().append() will then work properly. To remove the line on click, I added an event handler that will select the line just clicked and remove it.
var data = [[dataset_a], [dataset_b], [dataset_c], [dataset_d], [dataset_e]];
var xValue = function(d){return d.x;}
var yValue = function(d){return d.y;}
var lineFunction = d3.line()
.x(function(d) { return xScale(xValue(d)); })
.y(function(d) { return yScale(yValue(d)); });
var lines = d3.select("svg").selectAll("path")
lines.data(data)
.enter().append("path")
.attr("d", lineFunction)
.on("click", function(d){
d3.select(this).remove();
});

How to resize rectangle in d3 js

Right now I am able to resize a circle. I have created a rectangle using g.append('svg:rect') but I am not sure how to resize a rectangle in d3
This is what I have tried:
var boxWidth = 1300;
var boxHeight = 600;
var box = d3.select('body')
.append('svg')
.attr('class', 'box')
.attr('width', boxWidth)
.attr('height', boxHeight);
var drag = d3.behavior.drag()
.on('drag', function () {
g.selectAll('*')
.attr('cx', d3.event.x)
.attr('cy', d3.event.y);
});
var resize = d3.behavior.drag()
.on('drag', function () {
g.selectAll('.resizingContainer')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5) + 6;
});
g.selectAll('.draggableCircle')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5);
});
});
var g = box.selectAll('.draggableGroup')
.data([{
x: 65,
y: 55,
r: 25
}])
.enter()
.append('g');
g.append('svg:circle')
.attr('class', 'resizingContainer')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r + 6;
})
.style('fill', '#999')
.call(resize);
g.append('svg:circle')
.attr('class', 'draggableCircle')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r;
})
.call(drag)
.style('fill', 'black');
g.append('svg:rect')
.attr("width", 70)
.attr("height", 70)
.attr("x", 30)
.attr("y", 130)
.attr("rx", 6)
.attr("ry", 6)
.style("fill", d3.scale.category20c());
html:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js" charset="utf-8"></script>
<script src='d3.js' charset='utf-8'></script>
<style>
.box {
border: 1px black;
border-radius: 10px;
}
.resizingContainer {
cursor: nesw-resize;
}
</style>
</head>
<body>
<script src='drag.js'></script>
<div id="checks">
X-axis:<input type="checkbox" id="xChecked" checked/>
Y-axis:<input type="checkbox" id="yChecked" checked/>
</div>
</body>
</html>
Here's the live demo: https://jsbin.com/dejewumali/edit?html,js,output
Added rectangle resize for your code. Note that you need to use the bottom right corner for resizing (that was the easiest corner to add the resizing to :-))
var boxWidth = 1300;
var boxHeight = 600;
var box =
d3.select('body')
.append('svg')
.attr('class', 'box')
.attr('width', boxWidth)
.attr('height', boxHeight);
var drag = d3.behavior.drag()
.on('drag', function () {
g.selectAll('*')
.attr('cx', d3.event.x)
.attr('cy', d3.event.y);
});
var resize = d3.behavior.drag()
.on('drag', function () {
g.selectAll('.resizingContainer')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5) + 6;
});
g.selectAll('.circle')
.attr('r', function (c) {
return Math.pow(Math.pow(this.attributes.cx.value - d3.event.x, 2) + Math.pow(this.attributes.cy.value - d3.event.y, 2), 0.5);
});
});
var g = box.selectAll('.draggableCircle')
.data([{
x: 65,
y: 55,
r: 25
}])
.enter()
.append('g')
.attr('class', 'draggableCircle');
g.append('svg:circle')
.attr('class', 'resizingContainer')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r + 6;
})
.style('fill', '#999')
.call(resize);
g.append('svg:circle')
.attr('class', 'circle')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
.attr('r', function (d) {
return d.r;
})
.call(drag)
.style('fill', 'black');
var distance = function (p1, p2) {
return Math.pow(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2), 0.5);
}
var resize2 = d3.behavior.drag()
.on('drag', function () {
var c = g2.selectAll('.resizingSquare');
var s = g2.selectAll('.square');
var e = d3.event;
var x = Number(this.attributes.x.value);
var y = Number(this.attributes.y.value);
var w = Number(this.attributes.width.value);
var h = Number(this.attributes.height.value);
var c1 = { x: x, y: y };
var c2 = { x: x + w, y: y };
var c3 = { x: x + w, y: y + h };
var c4 = { x: x, y: y + h };
// figure out which corner this is closest to
var d = []
var m1 = distance(e, c1)
var m2 = distance(e, c2)
var m3 = distance(e, c3)
var m4 = distance(e, c4)
switch (Math.min(m1, m2, m3, m4)) {
case m3:
c
.attr('width', function () { return w + (e.x - c3.x) + 12 })
.attr('height', function () { return h + (e.y - c3.y) + 12 })
s
.attr('width', function () { return w + (e.x - c3.x) })
.attr('height', function () { return h + (e.y - c3.y) })
break;
}
});
var g2 = box.selectAll('.draggableSquare')
.data([{
x: 65,
y: 155,
width: 70,
height: 70
}])
.enter()
.append('g')
.attr('class', 'draggableSquare');
g2
.append('svg:rect')
.attr('class', 'resizingSquare')
.attr("width", function (d) {
return d.width + 12;
})
.attr("height", function (d) {
return d.height + 12;
})
.attr("x", function (d) {
return d.x - 6;
})
.attr("y", function (d) {
return d.y - 6;
})
.attr("rx", 6)
.attr("ry", 6)
.style("fill", '#999')
.call(resize2);
g2
.append('svg:rect')
.attr('class', 'square')
.attr("width", function (d) {
return d.width;
})
.attr("height", function (d) {
return d.height;
})
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("rx", 6)
.attr("ry", 6)
.style("fill", d3.scale.category20c());
JS Bin - https://jsbin.com/zenomoziso/1/edit
That said, if you are looking to use this beyond a proof of concept it's going to be very difficult. There are several problems with the above that will manifest once you have more elements
The container (g or g2) being used is a global variable.
The code is clunky (I just tacked on most of it based on the watches - there could be far more efficient ways of doing the same thing - e.g. you could not the start position onDragStart and use that to calculate the change in dimensions)
The code could be cleaner (think objects, better naming conventions, etc.). You might just want do d3.data... squares({ resize: true, move: true }) in your main block instead of all the individual steps.
You'll be better off searching for some existing diagramming library (why do all the math, when it's already done and tested :-)) - I found a blog with the canvas variant here - http://simonsarris.com/blog/510-making-html5-canvas-useful
Rectangle size is based on the width and height attributes. Thus to resize the rectangle you'll want to use something of this ilk:
d3.selectAll('rect')
.attr('width', function(c){
return d3.event.x - this.attributes.x.value;
}).attr('height', function(c){
return d3.event.y - this.attributes.y.value;
});
Inserted into your resize behaviour. If you do this and click to drag the circle and move to the origin of the rectangle, it'll then expand linearly with your cursor. It will likely work with little modification if the drag event was attached to the rectangle. Here is a simple application that showcases dragging and re-sizing of a rectangle.
For more reliability you'd want to select by a class identifier rather than a global rectangle select, but this is the basic idea.

nested circles in d3

Using d3.js, how would I modify the following code to add a nested, yellow-filled circle of radius "inner_radius" to each of the existing generated circles:
var circleData = [
{ "cx": 300, "cy": 100, "radius": 80, "inner_radius": 40},
{ "cx": 75, "cy": 85, "radius": 50, "inner_radius": 20}];
var svgContainer = d3.select("body").append("svg")
.attr("width",500)
.attr("height",500);
var circles = svgContainer.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.cx; })
.attr("cy", function (d) { return d.cy; })
.attr("r", function (d) { return d.radius; })
.style("fill", function (d) { return "red"; });
As imrane said in his comment, you will want to group the circles together in a g svg element. You can see the updated code here with relevant changes below.
var circles = svgContainer.selectAll("g")
.data(circleData)
.enter()
.append("g");
// Add outer circle.
circles.append("circle")
.attr("cx", function (d) { return d.cx; })
.attr("cy", function (d) { return d.cy; })
.attr("r", function (d) { return d.radius; })
.style("fill", "red");
// Add inner circle.
circles.append("circle")
.attr("cx", function (d) { return d.cx; })
.attr("cy", function (d) { return d.cy; })
.attr("r", function (d) { return d.inner_radius; })
.style("fill", "yellow");

Resources