Nested SVG selections in D3 - javascript

I have a list of n associative arrays.
[{'path': 'somepath', 'relevant': [7, 8, 9]}, {'path': 'anotherpath', 'relevant': [9], ...}
Within a large SVG, I want to: a) create rects ("buckets") whose dimensions are proportional to the lengths of their 'relevant' sublists, and b) create rects ("pieces"), for each of the elements in the sublists, placed "inside" their respective buckets.
After reading Mike Bostock's response to a similar question, I'm sure that I need to use group ("g") elements to group the pieces together. I can get the code below to produce the DOM tree that I want, but I'm stumped on how to code the y values of the pieces. At the point where I need the value, D3 is iterating over the subarrays. How can I get the index of the current subarray that I'm iterating over from inside it when i no longer points to the index within the larger array?
var piece_bukcets = svg.selectAll("g.piece_bucket")
.data(files)
.enter()
.append("g")
.attr("class", "piece_bucket")
.attr("id", function (d, i) { return ("piece_bucket" + i) })
.append("rect")
.attr("y", function (d, i) { return (i * 60) + 60; })
.attr("class", "bar")
.attr("x", 50)
.attr("width", function (d) {
return 10 * d["relevant"].length;
})
.attr("height", 20)
.attr("fill", "red")
.attr("opacity", 0.2)
var pieces = svg.selectAll("g.piece_bucket")
.selectAll("rect.piece")
.data( function (d) { return d["relevant"]; })
.enter()
.append("rect")
.attr("class", "piece")
.attr("id", function (d) { return ("piece" + d) })
.attr("y", ????) // <<-- How do I get the y value of d's parent?
.attr("x", function (d, i) { return i * 10; })
.attr("height", 10)
.attr("width", 10)
.attr("fill", "black");
Is there a method on d available to find the index of the node it's currently inside? In this case, is there a method I can call on a "piece" to find the index of its parent "bucket"?

You can use the secret third argument to the function:
.attr("y", function(d, i, j) {
// j is the i of the parent
return (j * 60) + 60;
})
There's a simpler way however. You can simply translate the g element and everything you add to it will fall into place.
var piece_buckets = svg.selectAll("g.piece_bucket")
.data(files)
.enter()
.append("g")
.attr("class", "piece_bucket")
.attr("transform", function(d, i) {
return "translate(0," + ((i*60) + 60) + ")";
})
.attr("id", function (d, i) { return ("piece_bucket" + i) });
piece_buckets.append("rect")
.attr("class", "bar")
.attr("x", 50)
.attr("width", function (d) {
return 10 * d["relevant"].length;
})
.attr("height", 20)
.attr("fill", "red")
.attr("opacity", 0.2);
var pieces = piece_buckets.selectAll("rect.piece")
.data(function (d) { return d["relevant"]; })
.enter()
.append("rect")
.attr("class", "piece")
.attr("id", function (d) { return ("piece" + d); })
.attr("x", function (d, i) { return i * 10; })
.attr("height", 10)
.attr("width", 10)
.attr("fill", "black");

Related

Resetting drag with multiple elements

I have multiple svg groups (each containing a circle and text) which I am dragging via d3-drag from an initial position. I have a rectangular hit zone that I only want one of these draggable groups in at a time. So whenever two groups are in the hit zone, I would like the first group that was in the hit zone to fade away and reappear in its initial position.
I have tried doing this via a function which translates the group back to its initial position by finding the current position of the circle shape and translating like:
translate(${-current_x}, ${-current_y})
This does translate the group back to the (0,0) position, so I have to offset by its initial position. I do this by setting the initial x and y values of the circle shape as attributes in the circle element and incorporating these into the translation:
translate(${-current_x + initial_x}, ${-current_y + initial_y})
Here is a block of my attempt:
https://bl.ocks.org/interwebjill/fb9b0d648df769ed72aeb2755d3ff7d5
And here it is in snippet form:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("initial_x", (d, i) => circleRadius * (2 * i + 1))
.attr("initial_y", circleRadius)
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise().classed("active", true);
}
function dragged() {
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
}
function dragEnded() {
d3.select(this).classed("active", false);
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
let current_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('cx');
let current_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('cy');
let initial_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_x');
let initial_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_y');
d3.select(`#${resetVar}`)
.transition()
.duration(2000)
.style('opacity', 0)
.transition()
.duration(2000)
.attr('transform', `translate(${-current_x}, ${-current_y})`)
.transition()
.duration(2000)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>
Here are the problems:
While using translate(${-current_x}, ${-current_y}) works, when I try using translate(${-current_x + initial_x}, ${-current_y + initial_y}), the translation uses very large negative numbers (for example, translate(-52640, -4640)).
While using translate(${-current_x}, ${-current_y}) works, when I try to drag this translated group again, the group immediately repeats the previous translate(${-current_x}, ${-current_y})
Your code runs into difficulties because you are positioning both the g elements and the children text and circles.
Circles and text are originally positioned by x/y attributes:
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
Drag events move the circles and text here:
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
And then we reset the circles and text by trying to offset the parent g with a transform:
d3.select(`#${resetVar}`).attr('transform', `translate(${-current_x}, ${-current_y})`)
Where current_x and current_y are the current x,y values for the circles and text. We have also stored the initial x,y values for the text, but altogether, this becomes a more convoluted then necessary as we have two competing sets of positioning coordinates.
This can be simplified a fair amount. Instead of positioning both the text and the circles, simply apply a transform to the parent g holding both the circle and the text. Then when we drag we update the transform, and when we finish, we reset the transform.
Now we have no modification of x,y/cx,cy attributes and transforms for positioning the elements relative to one another. No offsets and the parent g's transform will always represent the position of the circle and the text.
Below I keep track of the original transform with the datum (not an element attribute) - normally I would use a property of the datum, but you have non-object data, so I just replace the datum with the original transform:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
// Immovable placemarkers:
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
// Dragables
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter()
.append("g")
.datum(function(d,i) {
return "translate("+[circleRadius * (2 * i + 1),circleRadius]+")";
})
.attr("transform", (d,i) => d)
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise();
}
function dragged() {
d3.select(this).attr("transform","translate("+[d3.event.x,d3.event.y]+")")
}
function dragEnded() {
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
d3.select(`#${resetVar}`)
.transition()
.duration(500)
.style('opacity', 0)
.transition()
.duration(500)
.attr("transform", (d,i) => d)
.transition()
.duration(500)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>

Live Bar graph with d3 in js using data from realtime firebase

I am new to javascript and have been stuck at a problem for the better part of 2 weeks. I am trying to make a bar graph that updates in real time using data from Firebase. The structure of my database is:
title:
-------child1
-------child2
-------child3
-------child4
The data to firebase is provided from a python script that is working perfectly and is updating every child of title every 10 seconds.
I made a bar graph that is updating automatically via random number generation.
//Return array of 10 random numbers
var randArray = function() {
for(var i = 0, array = new Array(); i<10; i++) {
array.push(Math.floor(Math.random()*10 + 1))
}
return array
}
var initRandArray = randArray();
var newArray;
var w = 500;
var h = 200;
var barPadding = 1;
var mAx = d3.max(initRandArray)
var yScale = d3.scale.linear()
.domain([0, mAx])
.range([0, h])
var svg = d3.select("section")
.append("svg")
.attr("width", w)
.attr("height", h)
svg.selectAll("rect")
.data(initRandArray)
.enter()
.append("rect")
.attr("x", function(d,i) {return i*(w/initRandArray.length)})
.attr("y", function(d) {return h - yScale(d)})
.attr("width", w / initRandArray.length - barPadding)
.attr("height", function(d){return yScale(d)})
.attr("fill", function(d) {
return "rgb(136, 196, " + (d * 100) + ")";
});
svg.selectAll("text")
.data(initRandArray)
.enter()
.append("text")
.text(function(d){return d})
.attr("x", function(d, i){return (i*(w/initRandArray.length) + 20)})
.attr("y", function(d) {return h - yScale(d) + 15})
.attr("font-family", "sans-serif")
.attr("fill", "white")
setInterval(function() {
newArray = randArray();
var rects = svg.selectAll("rect")
rects.data(newArray)
.enter()
.append("rect")
rects.transition()
.ease("cubic-in-out")
.duration(2000)
.attr("x", function(d,i) {return i*(w/newArray.length)})
.attr("y", function(d) {return h - yScale(d)})
.attr("width", w / newArray.length - barPadding)
.attr("height", function(d){return yScale(d)})
.attr("fill", function(d) {
return "rgb(136, 196, " + (d * 100) + ")";
});
var labels = svg.selectAll("text")
labels.data(newArray)
.enter()
.append("text")
labels.transition()
.ease("cubic-in-out")
.duration(2000)
.text(function(d){return d})
.attr("x", function(d, i){return (i*(w/newArray.length) + 20)})
.attr("y", function(d) {return h - yScale(d) + 15})
.attr("font-family", "sans-serif")
.attr("fill", "white")
}, 3000)
Live bar chart on random number
I need to update the chart using the data from firebase. I already know how to connect firebase to js using the snapshot and have already tried it to no avail.
Also, need some help with the styling of the graph.
Please if anybody knows how I can finish this(its time sensitive).
Here's the code link in jsfiddle: Live bar chart d3
Thanks

How do append a <text> for every element of the data in a D3 svg

[Sorry the title was quite badly formulated. I would change it if I could.]
I'm searching for a way to append text elements from a array or arrays in the data.
EDIT: I can already do a 1 level enter .data(mydata).enter(). What I'm trying here is a second level of enter. Like if mydata was an object which contained an array mydata.sourceLinks.
cf. the coments in this small code snippet:
var c = svg.append("g")
.selectAll(".node")
.data(d.nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(i) {
return "translate(" + i.x + "," + i.y + ")"
})
c.append("text")
.attr("x", -200)
.attr("y", 30)
.attr("text-anchor", "start")
.attr("font-size","10px")
.text(function(d){
// d.sourceLinks is an array of elements
// console.log(d.sourceLinks[0].target.name);
// Here I would like to apped('text') for each of the elements in the array
// and write d.sourceLinks[i].target.name in this <text>
})
;
I tried a lot of different things with .data(d).enter() but it never worked and I got lot's of errors.
I also tried to insert html instead of text where I could insert linebreaks (that's ultimately what I'm trying to achieve).
I also tried
c.append("foreignobject")
.filter(function(i) { // left nodes
return i.x < width / 2;
})
.attr('class','sublabel')
.attr("x", -200)
.attr("y", 30)
.attr("width", 200)
.attr("height", 200)
.append("body")
.attr("xmlns","http://www.w3.org/1999/xhtml")
.append("div");
but this never showed up anywhere in my page.
Your question was not exactly clear, until I see your comment. So, if you want to deal with data that is an array of arrays, you can have several "enter" selections in nested elements, since the child inherits the data from the parent.
Suppose that we have this array of arrays:
var data = [
["colours", "green", "blue"],
["shapes", "square", "triangle"],
["languages", "javascript", "c++"]
];
We will bind the data to groups, as you did. Then, for each group, we will bind the individual array to the text elements. That's the important thing in the data function:
.data(d => d)
That makes the child selection receiving an individual array of the parent selection.
Check the snippet:
var data = [
["colours", "green", "blue"],
["shapes", "square", "triangle"],
["languages", "javascript", "c++"]
];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var groups = svg.selectAll("groups")
.data(data)
.enter()
.append("g")
.attr("transform", (d, i) => "translate(" + (50 + i * 100) + ",0)");
var texts = groups.selectAll("texts")
.data(d => d)
.enter()
.append("text")
.attr("y", (d, i) => 10 + i * 20)
.text(d => d);
<script src="https://d3js.org/d3.v4.min.js"></script>
Now, regarding your code. if d.nodes is an array of arrays, these are the changes:
var c = svg.append("g")
.selectAll(".node")
.data(d.nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(i) {
return "translate(" + i.x + "," + i.y + ")"
});//this selection remains the same
var myTexts = c.selectAll("myText")//a new selection using 'c'
.data(function(d){ return d;})//we bind each inner array
.enter()//we have a nested enter selection
.append("text")
.attr("x", -200)
.attr("y", 30)
.attr("text-anchor", "start")
.attr("font-size", "10px")
.text(function(d) {
return d;//change here according to your needs
});
You should use enter like this :
var data = ["aaa", "abc", "abd"];
var svg = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", function(d,i) {
return 20 + 50 * i;
})
.attr("y", 100)
.text(function(d) { return d; });
See this fiddle : https://jsfiddle.net/t3eyqu7z/

How to update a bar chart with accompanying text in d3?

I'm creating a bar chart as part of a bigger data visualization in d3. I want to be able to change the data in one part of the visualization and all the charts will be updated. A simplified version of the chart is as follows.
var dataset = [1, 3, 5, 3, 3];
...
var svg = d3.select("body #container").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
...
I create other charts like a map, circle etc with this svg element. The bar chart is implemented like this.
function bars(dataset) {
var barChart = g.selectAll("rect.bar")
.data(dataset)
.enter();
barChart.append("rect")
.attr("class", "bar")
.attr("x", function(d, i) { return i * 30 + 100; })
.attr("y", function(d) { return (height - 130) - d * 4;})
.attr("width", 25)
.attr("height", function(d) { return d * 4; });
barChart.append("text")
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * 30 + 103; })
.attr("y", function(d) { return (height - 130) - d/10 - 5;})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "darkgray");
}
Now this renders the bar chart fine but there is a function
...
.on("click", function() {
...
var newdata = [5, 2, 6, 2, 4]; // new values
g.selectAll("rect.bar").remove(); // This removes the bars
g.selectAll("text").remove(); // Problem here: All texts are removed
bars(newdata);
}
I have tried to transition the bar chart with new values with the .remove() function. This works for the bar rectangles because there are no othe bar charts but when I tried to remove the value labels like shown above all the other text elements were also removed. Is there a way to only update the text associated with the bars?
Have you tried applying a class to the text and only selecting those ones for removal?
e.g.
barChart.append("text")
.attr('class','label')
.text(function(d) { return d; })
then
g.selectAll(".label").remove();
Incidentally, if not all of the elements are being deleted between updates, then instead of removing all of the elements, have you considered using enter() and exit() to bind the new data to the existing elements and only remove the elements that are changing?
EDIT Like this:
function bars(dataset) {
var bar = g.selectAll(".bar").data(dataset);
bar.exit().remove();
bar.enter().append("rect").attr("class", "bar");
bar
.attr("x", function(d, i) { return i * 30 + 100; })
.attr("y", function(d) { return (height - 130) - d * 4;})
.attr("width", 25)
.attr("height", function(d) { return d * 4; });
var label = g.selectAll(".label").data(dataset);
label.exit().remove();
label.enter().append("text").attr("class", "label");
label
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * 30 + 103; })
.attr("y", function(d) { return (height - 130) - d/10 - 5;})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "darkgray");
}

D3 Plot array of circles

I've tried the circle plot example as the following:
var x=20, y=20, r=50;
var sampleSVG = d3.select("#viz")
.append("svg")
.attr("width", 800)
.attr("height", 600);
sampleSVG.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", r)
.attr("cx", x)
.attr("cy", y);
But I want to figure out how to plot without a loop a sequence of circles from an array like:
data = [
[10,20,30],
[20,30,15],
[30,10,25]
];
Maybe this example could help?
var data = [
[10,20,30],
[20,30,15],
[30,10,25]
];
var height = 300,
width = 500;
var svg = d3.select('body').append('svg')
.attr('height', height)
.attr('width', width)
.append('g')
.attr('transform', 'translate(30, 30)');
// Bind each nested array to a group element.
// This will create 3 group elements, each of which will hold 3 circles.
var circleRow = svg.selectAll('.row')
.data(data)
.enter().append('g')
.attr('transform', function(d, i) {
return 'translate(30,' + i * 60 + ')';
});
// For each group element 3 circle elements will be appended.
// This is done by binding each element in a nested array to a
// circle element.
circleRow.selectAll('.circle')
.data(function(d, i) { return data[i]; })
.enter().append('circle')
.attr('r', function(d) { return d; })
.attr('cx', function(d, i) { return i * 60; })
.attr('cy', 0);
Live Fiddle

Categories

Resources