Fade/change the color of an SVG Shape over time after being added to a canvas? - object

I have created an SVG map that plots tweets live containing particular keywords. I'm drawing each tweet to the screen as a circle (or dot), and after 50 tweets have been added to the map, the oldest one will disappear.
I'd like to have some sort of color decay for the circles depending on how long they've been on the map.
New tweets would pop onto the map and be red. As time passes, points already plotted on the map will slowly fade to black.
Here's where I add the circles to the map:
function mapTweet(tweetData) {
var tipText; // Ignore this. For tweet dot hovering.
var coordinates = projection([tweetData.geo.coordinates[1], tweetData.geo.coordinates[0]]);
addCircle(coordinates, tipText);
}
function addCircle(coordinates, tipText, r) {
tweetNumber++;
// too many tweets
if (tweetNumber == 50) {
tweetNumber = 0;
}
//removes expired circles
$('#' + tweetNumber).remove();
var rad;
//determine if radius size needs to be bumped
if (arguments.length == 3) {
rad = r;
} else {
rad = 3;
}
// add radar-style ping effect
map.append('svg:circle')
.style("stroke", "rgba(255,49,49,.7)")
.style("stroke-width", 1)
.style("fill", "rgba(0,0,0,0)")
.attr('cx', coordinates[0])
.attr('cy', coordinates[1])
.attr('r', 3)
.transition()
.delay(0)
.duration(2000)
.attr("r", 60)
.style("stroke-width", 2)
.style("stroke", "rgba(255,49,49,0.0001)").transition().duration(2000).remove();
// add circles representing tweets
map.append('svg:circle').attr("class", "tweetCircles")
.attr("id", tweetNumber)
.style("stroke", "rgba(255,49,49,.7)")
.style("stroke-width", 1)
.style("fill", "rgba(240,49,49,1)")
.attr('cx', coordinates[0])
.attr('cy', coordinates[1])
.attr('r', rad);
addTipsy(tipText, tweetNumber); // Ignore this. For tweet dot hovering.
}
Once a circle is drawn, does it have to be redrawn to change the color? Or can dots have their attributes changed after being added to the canvas?
How can I decay the color over, say, 20 seconds?

Append an animate element as a child of the circle
.append('svg:animate')
.attr('attributeName', 'fill')
.attr('from', 'red')
.attr('to', 'blue')
.attr('dur', '20s');
This will interpolate from red to blue or whatever colours you choose.

Related

SVG g hides when parent g is set to a height

I am relatively new to SVG. I am plotting a chart which will dynamically plot temperature or wind value on x,y axis respectively. I am good with x-axis positioning.
But when it comes to Y-Axis, position come right but if the value is higher than parent chart height, it get cut off as you can see in the picture.
Need help on how to force visible the circle/element even it exceeds the parent element width or height?
Here is how mark up gets generated
I am using d3.js for this.
Here is the code for generating circle with text
let svg = container
.append("svg")
.attr("width", width)
.attr("height", height+(height*0.7));
const chart = svg
.append("g")
.attr('class','chartwind')
.attr("transform", "translate(" + padding.left + "," + (padding.top) + ")")
chart.append("path")
.data(datapoints)
.attr("class", "line wind-line")
.style("stroke", colorScale("wind"))
.style('stroke-width', '2px')
.attr("d", windLine)
let windPoint = chart.selectAll('g.windpoint').data(datapoints).enter().append('g').attr('class', 'windpoint').attr('transform', function (d) {
return 'translate(' + (Math.round(xScale(d.parsedUtcDateTimeNow)) - 10) + ',' + yScale(d.windSpeed) + ')';
})
chart.selectAll('.windcircle')
.data(Infos)
.enter().append('circle')
.attr("class", "windcircle")
.style("stroke", colorScale("wind"))
.style('stroke-width', '2px')
.attr("cx", (d) => xScale(d.parsedUtcDateTimeNow))
.attr("cy", (d) => {
return yScale(d.windSpeed);
})
.attr('r', 3)
.style("fill", colorScale("wind"))
windPoint
.append('circle')
.attr("cx", 0)
.attr("cy", 0)
.attr("r", function (d) {
return 8;
})
.attr("fill", "green")
.attr("transform", "translate(0,-23)")
windPoint
.append('text')
.attr("fill", "white")
.attr("transform", "translate(0,-19)")
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "600")
.text((d) => {
return d.windGust;
});
windPoint
.append('text')
.attr('text-anchor', 'middle')
//.attr("transform","translate(0,5)")
.text((d) => {
return d.windSpeed;
});
Update
This is how I am constructing SVG height and width
function getSizesById(id) {
const container = document.querySelector(`#${id}`)
if (!container)
return null
return {
height: container.clientHeight,
width: container.clientWidth,
}
}
const { width, height } = (() => {
return getSizesById(containerID)
})()
const { width, height } = (() => {
return getSizesById(containerID)
})(),
padding = {
...size.padding,
top: 20,
bottom: 20
},
chartHeight = height - padding.bottom - padding.top,
chartWidth = width - padding.left - padding.right
const container = d3.select(`#${containerID}`)
let svg = container
.append("svg")
.attr("width", width)
.attr("height", height);
Note: SVG height (or chart) should look with in the DIV and only when the data points go over the scale, then we need data to be visible over the div height.
<g> elements do not have any inherent size. They are only a logical wrapper for a group of markup tags, and a place to give them some common properties. What restricts the visible parts of your chart is the <svg> element.
Your code shows that each datapoint is represented by grafic elements that span a bounding box of (-8 -31 16 35) (left - top - width - height). This is how much space you need to show all of it.
Remove the transform attribute from your .chartwind group. The space you need to show the complete graph is
left: lower boundary of your xScale range minus 8
top: lower boundary of your yScale range minus 31 (in the downward coordinate system)
width: extent of your xScale range plus 16
height: extent of your yScale range plus 35
Add some padding if you like.
Add a viewBox attribute with these four numbers to the <svg> element: viewbox="<left> <top> <width> <height>". The area described like this will then be fitted inside the available space of the <svg> element, without you having to do any further figuring out of transformations.

.style of child nodes in d3js

I'm trying to make a simple graph with nodes and links. I have "g" elements containing a circle and its text, and links on their own. I have, for example, this bit on code called on a mouseover event:
//check if circle is connected to current "viewed" (mouseover-ed)
//circle via a link referenced by "that" and paint it green if so
circles.filter(function() {
return d3.select(this).attr("index") == d3.select(that).attr("src");
}).attr("viewed",1).style("stroke", "green");
});
This was really a long shot as nodes is the 'g' element container and I wasn't sure what calling .style would do, but to my surprise it did change the color - but only for the text!
Is there a way to make it change the stroke style of the circle as well?
The declaration code:
var circles = svg.append("g")
.attr("class","nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
.append("g")
.attr("transform",function(d,i){d.x = getX(i);d.y=getY(i);return "translate(" + d.x + "," + d.y + ")";})
.attr("name", function(d){return d.name;})
.attr("viewed", 0)
.attr("focused", 0)
.attr("index", function(d, i) {return i;});
circles.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", node_radius_wo_pad)
.on("mouseover", function(){...};
circles.append("text")
.attr("text-anchor","middle")
.text(function(d){return d.name});
The reason this is working is that you haven't explicitly declared a stroke colour for the text and so it inherits what you set for the parent g element. To make this work for the circles, you have to select them explicitly:
var toChange = circles.filter(function() {
return d3.select(this).attr("index") == d3.select(that).attr("src");
});
toChange.attr("viewed", 1);
toChange.selectAll("circle").style("stroke", "green");
toChange.selectAll("text").style("stroke", "green");

Reordering svg dom with d3js

I'm using d3 to render an svg. I have an array of objects, each with a color property, like so:
data = [{'color': 'red'}, {'color': 'blue'}];
I have an update function to draw circles with those colors, like so:
function update(data) {
var circle = svg.selectAll('circle').data(data, function(d) {return d.color})
.enter()
.append('circle').attr('r', 50)
.attr('cx', function (d, i) {return 50 + (i * 50)}).attr('cy', 50)
.attr('fill', function (d) {return d.color});
circle.order();
}
My understanding is that the last line in the function, circle.order(), should reorder the nodes in the svg dom to match the order of the data. However, I change the array order and call the update function again, and it doesn't seem to do anything.
jsfiddle demo: http://jsfiddle.net/du7mh/
I need to control the dom order to bring certain elements to the foreground, since there's no z-index in svg. Any help would be appreciated, thanks!
The append selection doesn't have anything in it after update has run once. Setting circle equal to the both current and new elements works:
function update(data) {
var circle = svg.selectAll('circle').data(data, function(d) {return d.color});
circle.enter()
.append('circle').attr('r', 50)
.attr('cx', function (d, i) {return 50 + (i * 50)}).attr('cy', 50)
.attr('fill', function (d) {return d.color});
circle.order();
}
http://jsfiddle.net/du7mh/3/

D3.js semantic zoom misbehaving

I've been trying to teach myself D3.js, but I can't seem to get semantic zoom (zooming positions but not shapes) to work for me.
I've read the d3 zoom docs here, and attempted to functionally copy the svg semantic zoom example code
This is my code:
var X, Y, circle, circles, h, i, j, svg, transform, w, zoom, _i, _j;
w = 1200;
h = 600;
circles = [];
for (j = _i = 0; _i <= 6; j = ++_i) {
for (i = _j = 0; _j <= 12; i = ++_j) {
circles.push({r: 25, cx: i * 50, cy: j * 50});
}
}
X = d3.scale.linear()
.domain([0, 1])
.range([0, 1]);
Y = d3.scale.linear()
.domain([0, 1])
.range([0, 1]);
zoom = d3.behavior.zoom()
.x(X)
.y(Y)
.on("zoom", function() {
return circle.attr("transform", transform);
});
transform = function(d) {
return "translate(" + (X(d.cx)) + ", " + (Y(d.cy)) + ")";
};
svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.call(zoom)
.append("g");
circle = svg.selectAll("circle")
.data(circles)
.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("transform", transform);
Live version at jsfiddle.
This should be pretty simple. I'm creating grid of circles that should exactly touch when no zoom is applied (distance is 50 px, diameter is 50 px). When I zoom in, I expect the circles to spread apart, with the point under the mouse remaining stationary. I expect the zoom to be smooth and linear with applied mouse wheeling. The circles should remain the same size, though, so that they stop touching when I zoom in; they should overlap when I zoom out.
Instead, initially, the circles are spread out exactly twice as far as they should be. When I zoom in and out, the center point is not under the mouse (and moves around depending on how I pan). Zoom is highly nonlinear, asymptotically approaching a scale of 1 (circles touching) as I zoom out, and rapidly accelerating as I zoom in.
This seems really odd, and I can't spot significant differences between my code and the semantic zoom example, which works as expected. I conclude that I don't actually understand how D3 zoom is supposed to work. Can someone sort me out?
Your code is very close to being correct: Working demo.
Use scale to map the location of objects
Instead of saving the exact location of objects in them and then using scales with range and domain set to [0, 1], use the scales to do the mapping for you:
for (j = _i = 0; _i <= 6; j = ++_i) {
for (i = _j = 0; _j <= 12; i = ++_j) {
circles.push({
r: 25,
cx: i,
cy: j,
color: "#000"
});
}
}
X = d3.scale.linear()
.domain([0, 6])
.range([0, w]);
Y = d3.scale.linear()
.domain([0, 12])
.range([0, h]);
The change here is that now D3 knows about the aspect ratio of your viewport and in what proportions it should transform the scales so as to keep the point under the svg static under the mouse. Otherwise, it was trying to zoom in and out of a square, resulting in a jarring experience.
The problem was the initial position of the circles stacking up on the translation.
Live code with the problem pointed out and fixed, and a few other modifications:
var size = 600
var scale = 100
circles = []
for (var j = 0; j<6; j++) {
for (var i = 0; i<6; i++) {
circles.push({x: i*scale, y: j*scale })
}
}
var X = d3.scale.linear()
.domain([0,6*scale])
.range([0,size])
var Y = d3.scale.linear()
.domain([0,6*scale])
.range([0,size])
function transform(d) {
return "translate("+X(d.x)+", "+Y(d.y)+")"
}
var circle /*fwd declaration*/
var zoom = d3.behavior.zoom()
.x(X).y(Y)
.on("zoom", function () {
circle.attr("transform", transform)
})
var svg = d3.select("body").append("svg")
.attr("width", size).attr("height", size)
.call(zoom)
.append("g")
circle = svg.selectAll("circle")
.data(circles)
.enter().append("circle")
.attr("r", 20)
/*the problem was this initial offset interfering with the
translation we were applying, resulting in very strange behavior*/
/* .attr("cx", function (d) {return d.x})
.attr("cy", function (d) {return d.y})*/
.attr("transform", transform)
The "scale" parameter should do nothing, but if you add in those commented lines, it affects the initial position and causes the non-intuitive effects.
The original problems were:
Initial scale appeared to be more zoomed than it should have been.
Zooming out very var produced a noticeable nonlinear asymptotic effect.
Zooming out then panning around, then zooming back in did not work at all like expected, with the diagram sliding under the mouse instead of staying pinned.
All of these are straightforward consequences of the initial position:
The initial distances appeared bigger because we applied their original positions plus the zoom translation.
The nonlinear asymptotic effect was the zoom translation distances going to zero asymptotically (as expected), but the initially applied distances not going to zero, giving the appearance of a nonzero zoom asymptote.
While zoomed out, D3 thinks it's zoomed out more than the user does (because of the extra distances between circles), which means when a pan is applied, the center of the image as D3 tracks it is moving differently than what the user expects, which causes the effect of the zoom center not being under the mouse.
You can play with these effects to understand them by uncommenting the initial position lines and applying the same zoom actions with different scale parameters. Commenting them causes the circles to initially be all at screen-space 0,0, so that only the zoom distance translation is applied, which is what we want.
Props to musically_ut's answer for suggesting the smaller world-space coordinate scale, which shouldn't have made any difference, but did, which helped me identify the problem.

SVG markers don't orient properly on a d3.svg.diagonal curve used as a D3 force layout link

I'm a bit new to SVG and d3.js.
While drawing a graph with D3 force layout, I'm using a simple diagonal line generator and using marker-end to draw arrow heads.
When using arc instead of diagonal generator the arrow heads appear just fine. But using diagonal generator like in the code below doesn't produce proper markers:
var vis = this.vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.gravity(0.03)
.distance(120)
.charge(-800)
.size([w, h]);
var linkDiag = d3.svg.diagonal()
.projection(function(d)
{
return [d.x, d.y];
});
vis.append("svg:defs")
.selectAll("marker")
.data(["normal", "special", "resolved"])
.enter()
.append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M 0,-5 L 10,0 L0,5");
...and then also:
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; })
.attr("d", linkDiag)
.attr("marker-end", function(d) { return "url(#special)"; });
});
The markers are not oriented at all with the vertices.
Any help would be appreciated!
It only appears as if the arrows aren't pointing in the right direction because you're moving the arrowhead to a new position via refX and refY.
For example, check out this code which draws diagonals in various directions. The arrowheads appear correctly, with the exception of the one at 180 degrees, but that's probably due to a rounding error.
Now, try changing refX on line 10 to a value of, say, 5. Now, the arrowheads close to the horizontal appear incorrect. To see this more dramatically, try changing the value to 8.
What's happening is you're hiding part of the diagonal so the line appears to be ending at an earlier point, which is curved slightly differently from the actual end-point. The same thing will happen if the arrowhead is too large so that it overlays part of the curve. Note that for diagonals in d3, which are symmetrical bezier curves, the arrowheads should always appear pointing perfectly horizontally or vertically. You can see exactly what's happening by reducing the arrowhead's opacity.
Can you specify you question a little more?
However with even running you code I think you problem might be
.attr("orient", "auto")
Try to specify pos

Resources