D3 transition not working after update - javascript

I am trying to make a transition after drawing squares next to already drawn squares in my SVG container. My code to draw new squares next to the already drawn ones is as follow (not working):
var calculation = numToDrawSquares - numDrawnSquares;
var selectedGroup = d3.select("#" + source + "Group");
// Loop to draw the missing squares
for(var j = 0; j < calculation; j++)
{
var draw = selectedGroup.append("rect")
.attr("class", source)
.attr("x", x)
.attr("y", y)
.attr("width", 15)
.attr("height", 15)
.style("fill", color);
draw.transition()
.attr("y", y)
.duration(3000)
.delay(250);
}
My code for the already drawn squares is as follow (working):
// SVG container
container = d3.select("#" + source)
.append("svg")
.attr("class", "svgContainer")
.attr("width", 81)
.attr("height", 600);
rectangleGroup = container.append("g")
.attr("class", source + "_group" + " tooltip")
.attr("id", source + "Group")
// Drawing the squares
rectangle = rectangleGroup.append("rect")
.attr("class", source)
.attr("x", x)
.attr("y", 0)
.attr("width", 15)
.attr("height", 15)
.style("fill", "#" + color)
.transition()
.attr("y", y)
.duration(1000)
.delay(250);
Does anyone know why the transition in my first code snippet is not working? What can I do to fix that?

Related

How to zoom in a graph in JavaScript D3

I am trying to get Zoom to work, I am new to D3 and I find it very abstract and not intuitive. I recently finished a beginners course in JavaScript but D3 feels like a completely new language.
I found this topic which might help a bit.
D3 Zooming in graph
I also found the following code that created the graph on the web, the simplest I could find and I don't understand all of it. Now I wanna zoom in and used code that I also found on the web but which has to be adapted. I understood that much that the zoom variable at the top is calling a function called NeuerChart which has the actual zooming behaviour in it. It needs to zoom the graph and the axes when I spin the mousewheel.
In the end I need to implement this into a real problem, thanks. Using D3.v5.
<script>
let zoom = d3.zoom()
.scaleExtent([0.5, 10])
.extent([[0, 0], [width, height]])
.on('zoom', NeuerChart);
// Step 1
let min = 0;
let max = 100;
let x_arr = [];
let y_arr = [];
let s_arr = [];
let z_arr = [];
for (let i = 0; i < 360; i++) {
var r = Math.round(Math.random() * (max - min)) + min;
x_arr[i]= i;
y_arr[i]= r;
z_arr.push([x_arr[i],y_arr[i]]);
}
s_arr = y_arr.sort(function(a, b){return a - b});
let neu_arr = [];
let zz_arr = [];
for (let i = 0; i < 360; i++) {
neu_arr[i]= i;
zz_arr.push([neu_arr[i], s_arr[i]]);
}
console.log(z_arr);
console.log(zz_arr);
var dataset1 = zz_arr;
// Step 3
var svg = d3.select("svg"),
margin = 200,
width = svg.attr("width") - margin, //1700
height = svg.attr("height") - margin //700
// Step 4
var xScale = d3.scaleLinear().domain([0 , 365]).range([0, width]),
yScale = d3.scaleLinear().domain([0, 105]).range([height, 0]);
var g = svg.append("g")
.attr("transform", "translate(" + 100 + "," + 100 + ")");
// Step 5
// Title
svg.append('text')
.attr('x', width/2 + 100)
.attr('y', 100)
.attr('text-anchor', 'middle')
.style('font-family', 'Helvetica')
.style('font-size', 20)
.text('Line Chart');
// X label
svg.append('text')
.attr('x', width/2 + 100)
.attr('y', height - 15 + 150)
.attr('text-anchor', 'middle')
.style('font-family', 'Helvetica')
.style('font-size', 12)
.text('Zeitachse');
// Y label
svg.append('text')
.attr('text-anchor', 'middle')
.attr('transform', 'translate(60,' + 500 + ')rotate(-90)')
.style('font-family', 'Helvetica')
.style('font-size', 12)
.text('Wert');
// Step 6
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale).ticks(7).tickValues([0, 60, 120, 180, 240, 300, 360]));
g.append("g")
.call(d3.axisLeft(yScale));
// Step 7
svg.append('g')
.selectAll("dot")
.data(dataset1)
.enter()
.append("circle")
.attr("cx", function (d) { return xScale(d[0]); } )
.attr("cy", function (d) { return yScale(d[1]); } )
.attr("r", 3)
.attr("transform", "translate(" + 100 + "," + 100 + ")")
.style("fill", "#CC0000");
// Step 8
var line = d3.line()
.x(function(d) { return xScale(d[0]); })
.y(function(d) { return yScale(d[1]); })
.curve(d3.curveMonotoneX)
svg.append("path")
.datum(dataset1)
.attr("class", "line")
.attr("transform", "translate(" + 100 + "," + 100 + ")")
.attr("d", line)
.style("fill", "none")
.style("stroke", "#CC0000")
.style("stroke-width", "2")
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.attr("transform", "translate(" + 100 + "," + 100 + ")")
.call(zoom);
function NeuerChart () {
// recover the new scale
var newX = d3.event.transform.rescaleX(xScale);
var newY = d3.event.transform.rescaleY(yScale);
// update axes with these new boundaries
xAxis.call(d3.axisBottom(newX))
yAxis.call(d3.axisLeft(newY))
}
</script>
I added the code here in Codepen:
https://codepen.io/Dvdscot/pen/zYjpzVP
This is how it should work:
https://codepen.io/Dvdscot/pen/BaxJdKN
Problem is solved, see the code at Codepen:
`
Reset zoom
`https://codepen.io/Dvdscot/pen/zYjpzVP

Draw properly sized rectangle around text

I'm trying to draw a popup tooltip on hover when the user mouses over a pie chart. The following works, but the problem is the rectangle is a fixed size. How might one go about making the rectangle fit the size of the actual text?
// Draw rectangle
popup
.append("rect")
.attr("x", x + 5)
.attr("y", y - 5)
.attr("width", width)
.attr("height", height)
.attr("rx", 5)
.attr("ry", 5)
.style("fill", popupFillColor)
.style("stroke", stroke)
.style("stroke-width", 2);
// add text
popup
.append("text")
.attr("x", x + 10)
.attr("y", y + 10)
.text(e.seriesValue[0] + `: ${e.pValue} (${percent})`)
.style("font-family", "sans-serif")
.style("font-size", 14)
.style("fill", stroke);
Use getBBox:
const x = 50, y = 30;
const svg = d3.select('svg');
const rect = d3.select('svg')
.append('rect')
.style('fill', 'none')
.style('stroke', 'black');
const text = svg.append('text')
.text('This is my text')
.attr('x', x)
.attr('y', y)
const box = text.node().getBBox();
console.log(box)
const X_MARGIN = 20;
const Y_MARGIN = 10;
rect
.attr('x', box.x - X_MARGIN)
.attr('y', box.y - Y_MARGIN)
.attr('width', box.width + X_MARGIN * 2)
.attr('height', box.height + Y_MARGIN * 2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

How do I match up text labels in a legend created in d3

I am building a data visualization project utilizing the d3 library. I have created a legend and am trying to match up text labels with that legend.
To elaborate further, I have 10 rect objects created and colored per each line of my graph. I want text to appear adjacent to each rect object corresponding with the line's color.
My Problem
-Right now, an array containing all words that correspond to each line appears adjacent to the top rect object. And that's it.
I think it could be because I grouped my data using the d3.nest function. Also, I noticed only one text element is created in the HTML. Can anyone take a look and tell me what I'm doing wrong?
JS Code
const margin = { top: 20, right: 30, bottom: 30, left: 0 },
width = 1000 - margin.left - margin.right;
height = 600 - margin.top - margin.bottom;
// maybe a translate line
// document.body.append(svg);
const div_block = document.getElementById("main-div");
// console.log(div_block);
const svg = d3
.select("svg")
.attr("width", width + margin.left + margin.right) // viewport size
.attr("height", height + margin.top + margin.bottom) // viewport size
.append("g")
.attr("transform", "translate(40, 20)"); // center g in svg
// load csv
d3.csv("breitbartData.csv").then((data) => {
// convert Count column values to numbers
data.forEach((d) => {
d.Count = +d.Count;
d.Date = new Date(d.Date);
});
// group the data with the word as the key
const words = d3
.nest()
.key(function (d) {
return d.Word;
})
.entries(data);
// create x scale
const x = d3
.scaleTime() // creaters linear scale for time
.domain(
d3.extent(
data,
// d3.extent returns [min, max]
(d) => d.Date
)
)
.range([margin.left - -30, width - margin.right]);
// x axis
svg
.append("g")
.attr("class", "x-axis")
.style("transform", `translate(-3px, 522px)`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label-x")
.attr("x", "55%")
.attr("dy", "4em")
// .attr("dy", "20%")
.style("fill", "black")
.text("Months");
// create y scale
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.Count)])
.range([height - margin.bottom, margin.top]);
// y axis
svg
.append("g")
.attr("class", "y-axis")
.style("transform", `translate(27px, 0px)`)
.call(d3.axisLeft(y));
// line colors
const line_colors = words.map(function (d) {
return d.key; // list of words
});
const color = d3
.scaleOrdinal()
.domain(line_colors)
.range([
"#e41a1c",
"#377eb8",
"#4daf4a",
"#984ea3",
"#ff7f00",
"#ffff33",
"#a65628",
"#f781bf",
"#999999",
"#872ff8",
]); //https://observablehq.com/#d3/d3-scaleordinal
// craete legend variable
const legend = svg
.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 100)
.attr("transform", "translate(-20, 50)");
// create legend shapes and locations
legend
.selectAll("rect")
.data(words)
.enter()
.append("rect")
.attr("x", width + 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
// create legend labels
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
// returning an array as text
// });
svg
.selectAll(".line")
.data(words)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function (d) {
return color(d.key);
})
.attr("stroke-width", 1.5)
.attr("d", function (d) {
return d3
.line()
.x(function (d) {
return x(d.Date);
})
.y(function (d) {
return y(d.Count);
})(d.values);
});
});
Image of the problem:
P.S. I cannot add a JSfiddle because I am hosting this page on a web server, as that is the only way chrome can read in my CSV containing the data.
My Temporary Solution
function leg_labels() {
let the_word = "";
let num = 0;
for (i = 0; i < words.length; i++) {
the_word = words[i].key;
num += 50;
d3.selectAll(".legend")
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i + num;
})
// .attr("dy", "0.32em")
.text(the_word);
}
}
leg_labels();
Problem
Your problem has to do with this code
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
You are appending only a single text element and in the text function you are returning the complete array of words, which is why all words are shown.
Solution
Create a corresponding text element for each legend rectangle and provide the correct word. There are multiple ways to go about it.
You could use foreignObject to append HTML inside your SVG, which is very helpful for text, but for single words, plain SVG might be enough.
I advise to use a g element for each legend item. This makes positioning a lot easier, as you only need to position the rectangle and text relative to the group, not to the whole chart.
Here is my example:
let legendGroups = legend
.selectAll("g.legend-item")
.data(words)
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", function(d, i) {
return `translate(${width + 65}px, ${i * 20}px)`;
});
legendGroups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
legendGroups
.append("text")
.attr("x", 20)
.attr("y", 9)
.text(function(d, i) { return words[i].key; });
This should work as expected.
Please note the use of groups for easier positioning.

D3js drag&drop an object and detect where it is dropped.

I am working on dragging and dropping svg using d3js. There are two problems and I think they are related to each other.
When the circle is dropped it has to detect that it was dropped into the rectangle. Some of the examples that I have looked at uses x and y coordinates of the mouse, but I don't fully understand it.
Another problem is that the circle appears behind the rectangle. Is there a way to bring it to the front when the circle is moving around without changing the order of where the circle and rectangle are created i.e(create circle first and then rectangle).
var width = window.innerWidth,
height = window.innerHeight;
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
//create circle and space evenly
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var circle = d3.select("svg")
.append("circle")
.attr("cx", 50)
.attr("cy", 30)
.attr("r", 15)
.attr("transform", "translate(0,0)")
.style("stroke", "black")
.call(drag);
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
d3.select(this).attr("transform", "translate(" + [d3.event.x, d3.event.y] + ")");
}
function dragended(d) {
d3.event.sourceEvent.stopPropagation();
// here would be some way to detect if the circle is dropped inside the rect.
}
var ellipse = svg.append("rect")
.attr("x", 150)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green");
<script src="https://d3js.org/d3.v3.min.js"></script>
Any help is appreciated.
Updated to still include the bounding client rectangle, but iterate through any number of rectangles that exist. New Fiddle here.
Here's my solution to the problem. I used a great little "moveToBack" helper function seen here to move the rect to the back without changing the order in which it appears.
To get the positions of the circle and rectangle, I made heavy use of the vanilla js getBoundingClientRect() method. You can see all this together in this JS Fiddle.
var width = window.innerWidth,
height = window.innerHeight;
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
//create circle and space evenly
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var circle = d3.select("svg")
.append("circle")
.attr("r", 15)
.attr("transform", "translate(50,30)")
.style("stroke", "black")
.attr("id", "circle")
.call(drag);
d3.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
var rect = svg.append("rect")
.attr("x", 150)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green")
.attr("id", "rect")
.moveToBack();
var rect2 = svg.append("rect")
.attr("x", 350)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green")
.attr("id", "rect")
.moveToBack();
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
}
function dragended(d) {
// Define boundary
var rects = document.querySelectorAll("rect");
for (var i = 0; i < rects.length; i++) {
var rectDimensions = rects[i].getBoundingClientRect();
var xmin = rectDimensions.x;
var ymin = rectDimensions.y;
var xmax = rectDimensions.x + rectDimensions.width;
var ymax = rectDimensions.y + rectDimensions.height;
// Get circle position
var circlePos = document.getElementById("circle").getBoundingClientRect();
var x1 = circlePos.x;
var y1 = circlePos.y;
var x2 = circlePos.x + circlePos.width;
var y2 = circlePos.y + circlePos.height;
if(x2 >= xmin && x1 <= xmax && y2 >= ymin && y1 <= ymax) {
rects[i].setAttribute("fill", "red");
} else {
rects[i].setAttribute("fill", "green");
}
}
d3.event.sourceEvent.stopPropagation();
}

Text blocking circle click method d3js

To start I'm sure there is a much simpler way to do this then what I'm trying to do. I'm trying to zoom in on specific circles using d3js and have text overlaying the circle. The problem is that since the text is ontop of the circle the text blocks the onclick even that is fired when you click on the circle. Here is my code so far:
js
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var node1 = svg.append("g")
.append("circle")
.data([offset[0]])
.text(function(d){return d.label})
.attr("r", 25)
.style("fill","white")
.on("click", clicked);
node1.attr("cx", 530)
.attr("cy", 310)
.transition()
.delay(500)
.duration(1000)
.attr("r", 55)
.attr("cx", 530)
.attr("cy", 205);
d3.select('g').append('text')
.attr("id","orient")
.attr("dx", 510)
.attr("dy", 210)
.attr("width", 90)
.attr("height", 90)
.text(function(d){return offset[0].label});
var node2 = svg.append("g")
.append("circle")
.data([offset[1]])
.attr("r", 25)
.style("fill","white")
.on("click", clicked);
node2.attr("cx", 530)
.attr("cy", 310)
.transition()
.delay(500)
.duration(1000)
.attr("r", 55)
.attr("cx", 620)
.attr("cy", 310);
d3.select('g').append('text')
.attr("id","seperate")
.attr("dx", 590)
.attr("dy", 315)
.attr("width", 90)
.attr("height", 90)
.text(function(d){return offset[1].label});
function
function clicked(d) {
var imageSelected = this;
console.log("clicked");
var cx, cy, k, offset;
var setClass = d.swipe_class;
cx = d3.select(this).attr("cx");
cy = d3.select(this).attr("cy");
k = 2;
cx= cx - d.xoff;
cy= cy - d.yoff;
console.log("cy="+d.yoff +"cx="+ d.xoff);
svg.transition()
.duration(750)
.attr("transform", "translate(" + width/2 + "," + height/2 + ")scale(" + k + ")translate(" + -cx + "," + -cy + ")");
}
Is there a way to trigger the circles click event when I click the text ontop of it? Or maybe just a better way of doing this that would allow it?
You can set the text to ignore pointer events:
...
.append("text")
.attr("pointer-events", "none")
...

Categories