I am following this example:
https://bl.ocks.org/mbostock/6123708
the thing I can't understand is how I can possibly add new circles when a button is clicked, for example:
d3.tsv("dots.tsv", dottype, function (error, dots) {
container.append("g")
.attr("class", "dot")
.selectAll("circle")
.data(dots)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.call(drag);
});
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
return d;
}
self.addNode = function () {
container.append('g')
.attr('class', 'dot')
.append('circle')
.attr('r', 35)
.attr('cx', (i * 100) + cx)
.attr('cy', (i * 100) + cy)
//.style('fill', 'purple')
.call(drag);
i++;
};
The first part is the same as the example, I then created a function to add a single circle inside the container, the problem is that when I drag the new added circle I can move only the external G element, thus moving every other circle together.
I can't understand why, as the functions are the same (I removed even the style 'fill' to be sure)
You are giving your layout a data in .data(dots) but when you are adding a node in your addNode function, the layout is unaware of this new data. What you want is to add/push the new node data to your data array(dots) and recall the drawing function.
Therefore, you should cut the code under d3.tsv into a function to call it again when you update the data.
Related
I have a scatter plot which uses three different CSV's. The following is the Javascript that controls the dots used for one of them:
svg.append('g')
.selectAll("dot")
.data(files[2])
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.Index); } )
.attr("cy", function (d) { return y(d.Value); } )
.attr("r", 4)
.style("fill", "#d3d3d3")
Currently, the dots from this CSV are light grey. However, how could I make the first point listed in this CSV, or any other I want, a different color? Thanks in advance.
Welcome to StackOverflow!
In order to achieve what you want, you could use a callback in the fill style, just like you are doing in cx and cy, but adding a second parameter i, this callback's second parameter is the index (zero based), then you can conditionally return one value or another. Something like this:
svg.append('g')
.selectAll("dot")
.data(files[2])
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.Index); } )
.attr("cy", function (d) { return y(d.Value); } )
.attr("r", 4)
.style("fill", function (d, i) {
if(i === 0) {
return "#f05050"
} else {
return "#d3d3d3"
}
})
I'm trying to create a dynamic visualization using circles that 'spread' out over some amount of time and all of the circles have the same centerpoint.
I have a separate script that creates the circles and stores the data in a JSON file - the first circle in the JSON file is the smallest circle on top of the image linked above.
Please see code snippet below. Basically, the script appends the circle data into circles into an svg with visibility set to none. The script reveal the circles one by one.
In the appending function, I tried using the .lower() function to reverse the order that the circles are appended to the svg because if I were to append in the order that the JSON file is in, each consecutive circle would hide the one below it. But then the animation plots backwards, where the larger circle plots first.
In the revealing function, I then tried adding a similar '.lower()' function to the transition method so each consecutive circle would reveal behind the previously revealed circle but then the code breaks. I'm just at a loss here - any pointers would be much appreciated.
html,
body,
#svg {
background-color: #FFFFFF;
}
<html>
<head>
<meta charset="utf-8" />
<title>Visualizer</title>
</head>
<body>
<div>
<button onclick="plotStatically(0, 0, 'testingcircle.json')">Draw Static ►</button>
<button onclick="plotConsecutively(0, 0, 'testingcircle.json')">Draw Dynamic ►</button>
</div>
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<script>
function plotConsecutively(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx + x;
})
.attr('cy', function(d) {
return d.cy + y;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll("circle")
.transition()
.delay(function(d, i) {
return 3.4 * i;
})
.duration(10)
.attr('visibility', 'visible');
})
}
function plotStatically(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
});
})
}
</script>
</body>
</html>
I think you were pretty much there.
As you said, the larger circles need to be appended to the svg first so that they don't block out the smaller circles beneath them. I think this is most easily done simply by reversing the order of the data array just after you get the results of the json file:
d3.json(nameFile).then(function(data) {
data = data.reverse();
...
Then, in order to show the circles from the inside out, you can change your delay function so that the items at the end of the array that you want to show first (the smaller circles) have the smallest delay, and the items at the beginning of the array that you want to show last (the larger circles) have the largest delay.
The third argument to the delay() function is the NodesList containing all the selected DOM elements, so you can use the length property of that array in your calculations.
...
.delay(function(d, i, circleNodes) {
return 3.4 * ((circleNodes.length - 1) - i);
})
...
let data = [
{"r":5,"cx":100,"cy":100,"fill":"red"}, {"r":10,"cx":100,"cy":100,"fill":"magenta"},{"r":15,"cx":100,"cy":100,"fill":"orange"},{"r":20,"cx":100,"cy":100,"fill":"green"},{"r":25,"cx":100,"cy":100,"fill":"blue"}
];
data = data.reverse();
function plotConsecutively() {
var svg = d3.select('#svg')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll('circle')
.transition()
.delay(function(d, i, nodes) {
return 150 * ((nodes.length - 1) - i);
})
.duration(10)
.attr('visibility', 'visible');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button onclick="plotConsecutively()">Draw Dynamic ►</button>
<div id="svg"></div>
I have a demo here
Its a line bar chart using D3 in an Angular app.
I want the chart to be responsive so when the page is resized the chart width will increase and the height will be stay the same.
I'm doing this by capturing the window resize and then calling the function that draws the chart.
This works for the axis but I cant get the line and points to redraw.
I think it's to do with the way I'm trying to us the update pattern
How can I use the update pattern to redraw this line graph
const that = this;
const valueline = d3.line()
.x(function (d, i) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.y(function (d) {
return that.y(d.value);
});
this.x.domain(data.map((d: any) => d.date));
this.y.domain(d3.extent(data, function (d) {
return d.value
}));
const thisLine = this.chart.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
const totalLength = thisLine.node().getTotalLength();
thisLine.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength);
thisLine.transition()
.duration(1500)
.attr("stroke-dashoffset", 0)
let circle = this.chart.selectAll("line-circle")
.data(data);
circle = circle
.enter()
.append("circle")
.attr("class", "line-circle")
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.attr("r", 4)
.attr("cx", function (d) {
return that.x(d.date) + 0.5 * that.x.bandwidth();
})
.attr("cy", function (d) {
return that.y(d.value);
})
circle
.exit()
.remove()
You have problems in both circles' selection and the line selection.
The circles' selection:
You're selecting "line-circle". Instead of that, you have to select by class: ".line-circle";
You're reassigning the circle selection:
circle = circle.enter()//etc...
Don't do that, otherwise circle will point to the enter selection, not to the update selection anymore. Just do:
circle.enter()//etc...
The path:
You're appending a new path every time you call the function. Don't do that. Instead, select the existing path and change its d attribute, or append a new path if there is none. Both behaviours can be achieved with this code:
let thisLine = this.chart.selectAll(".line")
.data([data]);
thisLine = thisLine.enter()
.append("path")
.attr("class", "line")
.merge(thisLine)
.attr("d", valueline);
Here is your forked code: https://stackblitz.com/edit/basic-scatter-mt-vvdxqr?file=src/app/bar-chart.ts
first of all sorry for the long title, I couldn't come up with a better one to explain my problem.
What I have is a scatterplot, with circles that have different ids based on the d.FamilyName column in my csv. Some circles share the same id.
On the right of that scatterplot, I set up a div, containing a list of all the d.Familyname values, nested to have them show just once. The id of every text is also set by d.FamilyName.
To improve readability of the scatterplot, since it has many values, I was planning to add an event listener on mouseover on the text, which should then modify the radius of the circles sharing the same id as the text.
Every circle is plotted inside the var circle and the text is plotted inside a div, here's the code for the circle and the text part:
var circle = svg.append("g")
.attr("id", "circles")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.SquadraturaInterna_o); })
.attr("cy", function (d) { return y(d.SquadraturaEsterna_o); })
.attr("r", 2)
After the radius attribute there's an event listener to show other values of the specified circle (the name and coordinates) but they are not relevant to my problem I think.
The text part:
d3.select("#elenco")
.select("#value")
.selectAll("text")
.data(nested)
.enter()
.append("p")
.append("text")
.attr("id", function (i) { return (i).key; })
.text(function (i) { return (i).key; })
.on("mouseover", function (d, i) {
if (this.id == circle.id)
{d3.select("circle")
.attr("r", 5);
}
else {d3.select("circle").attr("r", 1);}
;})
.on("mouseout", function (d, i) {
d3.selectAll("circle")
.attr("r", 2);
});
The problem is of course on the if statement in the mouseover.
Any hint on how to solve this would be really appreciated, thanks!
EDIT: Thanks to #Lars' reply I was able to make this work with a little edit to his code, like so:
.on("mouseover", function (d) {
var sel = this.id;
circle.filter(function() { return this.id === sel; }).attr("r", 5); })
.on("mouseout", function (d, i) {
d3.selectAll("circle")
.attr("r", 2); }
);
As an alternative to #musically_ut's approach, you can also use the .filter() function to modify only the elements you want.
.on("mouseover", function(d) {
circle.filter(function() { return this.id === d.FamilyName; }).attr("r", 5);
})
.on("mouseout", function(d) {
circle.filter(function() { return this.id === d.FamilyName; }).attr("r", 1);
})
I think you are looking for this:
.on("mouseover", function (d, i) {
circles.attr('r', function (circle_d, i) {
return (d.id == circle_d.id) ? 5 : 1;
});
})
In D3, the this in the accessor functions refers to the DOM element. If you wanted to compare the id of the DOM element with circle's data's ids, then you could do something like this kind: d3.select(this).attr('id') == circle_d.id (cache d3.select(this).attr('id') for performance reasons).
I have a map where circles (origin of people) appear when clicking on a legend.
Additionally, it is possible to zoom in, and then, circles (and country path) are transformed (using d3.behavior.zoom).
Though, if I first zoom in, and then click on the legend, circles do not appear at the right places. How can I solve this problem and append them at the right coordinates (within the zoomed map).
Any ideas? I'm sure the solution is not that difficult, but I'm stucked.
See (http://wahrendorf.de/circlemapping/world_question.html) for an example.
Thanks,
Morten
You need to take into account d3.event.translate and d3.event.scale when you draw the circles. The easiest way to do this is to factor out your zoom function so that it may be called by the circle drawing function.
var translate = [0,0];
var scale = 1;
var zoom_function = function() {
canvas.selectAll("path")
.attr("transform","translate("+translate.join(",")+")scale("+scale+")");
canvas.selectAll("circle.origin")
.attr("transform","translate("+translate.join(",")+")scale("+scale+")")
.attr("r", function(d) { return radius/scale; });
};
var zoom = d3.behavior.zoom().scaleExtent([1,6])
.on("zoom",function() {
translate = d3.event.translate;
scale = d3.event.scale;
zoom_function();
});
// ... The rest of the code ...
canvas.append("text")
.text("show circles")
.attr("x", 30 ) .attr("y", 480 )
.attr("dy", ".35em")
.on("click", function(d) {
/// load data with long/lat of circles
d3.csv("./World_files/places_q.csv", function(error, origin) {
canvas.selectAll("circle.origin").remove();
canvas.selectAll("circle.origin")
.data(origin)
.enter()
.append("circle")
.attr("cx", function(d) {return projection([d.originlong, d.originlat])[0];})
.attr("cy", function(d) {return projection([d.originlong, d.originlat])[1];})
.attr("r", 2)
.style("fill", "red")
.style("opacity", 0.5)
.attr("class", "origin");
// Call the zoom function here to fix the placement of the circles.
zoom_function();
});
});
You will need to track the last known d3.event.translate and d3.event.scale values since they will be undefined when you are drawing the circles.