I have build a scatter plot in d3 v4 using following link: scatter plot
I have also used tipsy plugin for tooltip.
Now i wanted to add guidelines in this chart that is show guideline when user hovers over a circle and hide guidelines when out of focus. For this i stumbled upon a code which i tried to use but it is not showing anything.
Following is the code which i used:
var circles = svg.selectAll("circle").data(dataset).enter().append("circle");
circles.attr("cx",function(d){
return xScale(d[indicator1]);//i*(width/dataset.length);
})
.attr("cy",function(d){
return yScale(d[indicator2]);
})//for bottom to top
.attr("r", 10);
circles.attr("fill",function(d){
return "#469DDA";
});
circles.attr("stroke",function(d){
return "white";
});
circles.attr("class", "circles");
svg.style('pointer-events', 'all')
// what to do when we mouse over a bubble
var mouseOn = function() {
var circle = d3.select(this);
// transition to increase size/opacity of bubble
circle.transition()
.duration(800).style("opacity", 1)
.attr("r", 16).ease("elastic");
// append lines to bubbles that will be used to show the precise data points.
// translate their location based on margins
svg.append("g")
.attr("class", "guide")
.append("line")
.attr("x1", circle.attr("cx"))
.attr("x2", circle.attr("cx"))
.attr("y1", +circle.attr("cy") + 26)
.attr("y2", h - margin.t - margin.b)
.attr("transform", "translate(40,20)")
.style("stroke", "#ccc")
.transition().delay(200).duration(400).styleTween("opacity",
function() { return d3.interpolate(0, .5); })
svg.append("g")
.attr("class", "guide")
.append("line")
.attr("x1", +circle.attr("cx") - 16)
.attr("x2", 0)
.attr("y1", circle.attr("cy"))
.attr("y2", circle.attr("cy"))
.attr("transform", "translate(40,30)")
.style("stroke", "#ccc")
.transition().delay(200).duration(400).styleTween("opacity",
function() { return d3.interpolate(0, .5); });
// function to move mouseover item to front of SVG stage, in case
// another bubble overlaps it
/* d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
// skip this functionality for IE9, which doesn't like it
if (!$.browser.msie) {
circle.moveToFront();
}*/
};
// what happens when we leave a bubble?
var mouseOff = function() {
var circle = d3.select(this);
// go back to original size and opacity
circle.transition()
.duration(800).style("opacity", 1)
.attr("r", 10).ease("elastic");
// fade out guide lines, then remove them
d3.selectAll(".guide").transition().duration(100).styleTween("opacity",
function() { return d3.interpolate(.5, 0); })
.remove()
};
// run the mouseon/out functions
circles.on("mouseover", mouseOn);
circles.on("mouseout", mouseOff);
$('.circles').tipsy({
gravity: 'w',
html: true,
title: function() {
var d = this.__data__;
return "State: "+d.States+"<br>"+indicator1+" "+d[indicator1]+"<br>"+indicator2+" "+d[indicator2];
}
});
I am getting following result now:
When i checked the browser console i am getting following error:
If you are using d3.v4 , I think problem lays with transition's ease method
You should provide easing constant, instead of plain string
So, instead of using
circle.transition()
.duration(800).style("opacity", 1)
.attr("r", 16).ease("elastic");
You should write
circle.transition()
.duration(800).style("opacity", 1)
.attr("r", 16).ease(d3.easeElastic) // change
Related
I'm playing with some d3 code - to create the y axis I do the following :
function renderYAxis(svg) {
var yAxis = d3.svg.axis()
.orient("left")
.scale(_y.range([quadrantHeight(), 0]))
.tickFormat(d3.format("s"));
axisData = _currentData.filter(function(row) {
if ((row['filter1'] === _filter1)) {
return true;
}
}).filter(function(row) {
if ((row['filter2'] === _filter2)) {
return true;
}
}).map(function(d) {
return {
y: +d["Y"]
};
});
var minY2 = d3.min(axisData, function(d) { return d.y });
if (minY2 > 0) {
minY2 = 0;
};
_y.domain([minY2, d3.max(axisData, function(d) { return d.y })])
if (!_axesYG) {
_axesYG = svg
.append("g")
.attr("class", "y axis");
}
_axesYG
.attr("transform", function() {
return "translate(" + xStart() + "," + yEnd() + ")";
})
.transition()
.duration(1000)
.call(yAxis);
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> maybe following needs changing somehow? >>>>>>>>>>>>>>
d3.selectAll("g.y g.tick")
.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
}
The chart has a transition but after transitioning several times some of the grid lines are reproducing and being laid on top of each other - so producing some thicker lines. I've marked above where I think the problem may be, I'm unsure how to change this code - is there a standard approach?
A full working example of the behavior is saved here: http://plnkr.co/edit/JD52TfAddZSpNR3oaMRv?p=preview
If you hit the button several times you will see it is the common grid lines that are shared before and after the transition that are being recreated and overlaid. These two:
Any help much appreciated.
An easy solution is just setting the tick width with a negative value:
.innerTickSize(-quadrantWidth());
That way, you don't have to worry about appending, removing or updating the lines, and you won't have duplicated elements: the axis generator takes care of all that for you.
Here is the updated plunker: http://plnkr.co/edit/BoP4hEkILlwJzRuCJFBD?p=preview
EDIT: you mentioned in your answer that you're having problems with Nick Zhu's approach. That's because your selection is not correct. It should be something like this:
var lines = d3.selectAll("g.y g.tick")
lines.selectAll(".grid-line")
.remove();
lines.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
Here is the respective plunker: http://plnkr.co/edit/189hJBepdVVreLghBgc0?p=preview
Here is a simple fix (hack), since the original code structure is hard to change to follow General Update Pattern correctly:
// remove old ones
d3.selectAll(".grid-line.y-axis")
.remove();
// draw new ones
// add a new class y-axis to avoid deleting the x axis above
d3.selectAll("g.y g.tick")
.append("line")
.classed("grid-line y-axis", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
http://plnkr.co/edit/wdQmllRrrILtXsarXqLY?p=preview
The more correct approach is to follow the General Update Pattern: https://bl.ocks.org/mbostock/3808234
just for completeness I thought I'd add the following, which I found in Nick Qi Zhu's book. I think it follows the general update pattern as well as grid-lines can. Although even adding this I still get a reproduction of the grid-lines!
function renderYGridlines() {
var lines = d3.selectAll("g.y g.tick")
.select("grid-line y-axis")
.remove();
lines = d3.selectAll("g.y g.tick")
.append("line")
.classed("grid-line", true)
lines.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
}
jsfiddle https://jsfiddle.net/z7ju3z1q/3/
The problem is, I can't make titles stick with groups with working simulation.
Changing line 76 from simulation.nodes(nodes); to simulation.nodes(node); breaks simulation (apparently), but makes title stick to circles. As I understand, the problem is that dragging working with circles but not with groups, for some reason. And that's is the problem Iwe been facing whole day.
I tried describing title like this (line 38)
var title = g.selectAll("text");
And then adding it to tick (line 84)
function ticked() {
title.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
}
As I understand, the problem is that dragging working with circles but not with groups, for some reason
You are manipulating the nodes on drag with cx and cy properties. g elements do not have these, so this will not achieve what you want, even if node contained groups rather than circles:
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
As noted,node = g.append("circle") means that you aren't actually manipulating the g elements anyways, which is why your circles move on tick or drag.
Instead, keep node a selection of g elements, and manipulate the transform property:
// the group representing each node:
node = node.enter().append("g").merge(node);
// the circle for each group
node.append("circle")
.classed('node', true).attr('id', id)
.text(id)
.attr("r", 25).attr("fill", function(d) { return color(d.id); });
// the text for each group
node.append("text")
.classed('text', true)
.attr("text-anchor", "start")
.attr("dx", 6).text(id).merge(node);
Then, on click or drag events, just update the transform:
Tick:
node.attr("transform",function(d) { return "translate("+d.x+","+d.y+")" ;});
Drag:
d.x = d3.event.x;
d.y = d3.event.y;
d3.select(this).attr("transform",function(d) { return "translate("+d.x+","+d.y+")" ;});
Here's an updated fiddle.
Working through the excellent Interactive Data Visualization for the Web book and have created (a monstrosity of a) script to create an interactive bar chart that:
Adds a new bar to the end when clicking on the svg element
Generates a new set of 50 bars when clicking on the p element
I have added a mouseover event listener to change the color of the bars when hovering over. The problem is that bars added via 1. above are not changing color. As far as I can tell, the bars are getting selected properly, but for whatever reason, the mouseover event is never being fired for these bars:
svg.select(".bars").selectAll("rect")
.on("mouseover", function() {
d3.select(this)
.transition()
.attr("fill", "red");
})
Thanks in advance for your help, it is always greatly appreciated.
Here is the full code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Interactive Data Visualization for the Web Program-Along</title>
<style>
/* axes are made up of path, line, and text elements */
.axis path,
.axis line {
fill: none;
stroke: navy;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
/* color is CSS property, but need SVG property fill to change color */
fill: navy;
}
</style>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<p>Click on this text to update the chart with new data values.</p>
<script type="text/javascript">
var n = 50;
var domain = Math.random() * 1000;
function gen_data(n, domain) {
var d = [];
for (var i = 0; i < n; i++) {
d.push(
{ id: i, val: Math.random() * domain }
);
}
return d;
}
// define key function once for use in .data calls
var key = function(d) {
return d.id;
};
var dataset = gen_data(n, domain);
// define graphic dimensions
var w = 500, h = 250, pad = 30;
// get input domains
var ylim = d3.extent(dataset, function(d) {
return d.val;
});
// define scales
var x_scale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, w - pad], 0.15);
var y_scale = d3.scale.linear()
.domain([ylim[0], ylim[1] + pad]) // could have ylim[0] instead
// range must be backward [upper, lower] to accommodate svg y inversion
.range([h, 0]); // tolerance to avoid clipping points
var color_scale = d3.scale.linear()
.domain([ylim[0], ylim[1]])
.range([0, 255]);
// create graphic
var svg = d3.select("body").append("div").append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("class", "bars")
.selectAll(".bars rect")
.data(dataset)
.enter()
.append("rect")
.attr({
x: function(d, i) {
return x_scale(i) + pad;
},
y: function(d) {
return y_scale(d.val);
},
width: x_scale.rangeBand(), // calculates width automatically
height: function(d) { return h - y_scale(d.val); },
opacity: 0.6,
fill: function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
}
});
// add axes
var yAxis = d3.svg.axis()
.scale(y_scale) // must be passed data-to-pixel mapping (scale)
.ticks(3) // optional (d3 can assign ticks automatically)
.orient("left");
// since function, must be called
// create <g> to keep things tidy, to style via CSS, & to adjust placement
svg.append("g")
.attr({
class: "axis",
transform: "translate(" + pad + ",0)"
})
.call(yAxis);
// add event listener for clearing/adding all new values
d3.select("p")
.on("click", function() {
// generate new dataset
dataset = gen_data(n, domain);
// remove extra bars
d3.selectAll(".bars rect")
.data(dataset, function(d, i) { if (i < 50) { return d; }})
.exit()
.transition()
.attr("opacity", 0)
.remove();
// update scales
x_scale.domain(d3.range(dataset.length))
.rangeRoundBands([0, w - pad], 0.15);
ylim = d3.extent(dataset, function(d) {
return d.val;
});
y_scale.domain([ylim[0], ylim[1] + pad]);
// update bar values & colors
d3.selectAll(".bars rect")
.data(dataset)
.transition()
.duration(500)
.attr("x", function(d, i) { return x_scale(i) + pad; })
.transition() // yes, it's really this easy...feels like cheating
.delay(function(d, i) { return i * (1000 / dataset.length); }) // set dynamically
.duration(1000) // optional: control transition duration in ms
.each("start", function() {
// "start" results in immediate effect (no nesting transitions)
d3.select(this) // this to select each element (ie, rect)
.attr("fill", "magenta")
.attr("opacity", 0.2);
})
.attr({
y: function(d) { return y_scale(d.val); },
height: function(d) { return h - y_scale(d.val); }
})
.each("end", function() {
d3.selectAll(".bars rect")
.transition()
// needs delay or may interrupt previous transition
.delay(700)
.attr("fill", function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
})
.attr("opacity", 0.6)
.transition()
.duration(100)
.attr("fill", "red")
.transition()
.duration(100)
.attr("fill", function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
});
});
// update axis (no need to update axis-generator function)
svg.select(".axis")
.transition()
.duration(1000)
.call(yAxis);
});
// extend dataset by 1 for each click on svg
svg.on("click", function() {
// extend dataset & update x scale
dataset.push({ id: dataset.length, val: Math.random() * domain });
x_scale.domain(d3.range(dataset.length));
// add this datum to the bars <g> tag as a rect
var bars = svg.select(".bars")
.selectAll("rect")
.data(dataset, key);
bars.enter() // adds new data point(s)
.append("rect")
.attr({
x: w,
y: function(d) {
return y_scale(d.val);
},
width: x_scale.rangeBand(), // calculates width automatically
height: function(d) { return h - y_scale(d.val); },
opacity: 0.6,
fill: function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
}
});
// how does this move all the other bars!?
// because the entire dataset is mapped to bars
bars.transition()
.duration(500)
.attr("x", function(d, i) {
return x_scale(i) + pad;
});
});
// add mouseover color change transition using d3 (vs CSS)
svg.select(".bars").selectAll("rect")
.on("mouseover", function() {
d3.select(this)
.transition()
.attr("fill", "red");
})
.on("mouseout", function(d) {
d3.select(this)
.transition()
.attr("fill", function() {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
})
.attr("opacity", 0.6);
})
// print to console when clicking on bar = good for debugging
.on("click", function(d) { console.log(d); });
</script>
</body>
</html>
UPDATE:
Thanks to Miroslav's suggestion, I started playing around with different ways to resolve the issue and came across Makyen's answer to this related SO post.
While I imagine there is a more performant way to handle this, I have decided to rebind the mouseover event listener each time the mouse enters the svg element using the following code:
svg.on("mouseover", mouse_over_highlight);
// add mouseover color change transition using d3 (vs CSS)
function mouse_over_highlight() {
d3.selectAll("rect")
.on("mouseover", function () {
d3.select(this)
.transition()
.attr("fill", "red");
})
.on("mouseout", function (d) {
d3.select(this)
.transition()
.attr("fill", function () {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
})
.attr("opacity", 0.6);
})
// print to console when clicking on bar = good for debugging
.on("click", function (d) {
console.log(d);
});
}
Reason your event fires only for first bar and not the dynamic ones is because of the way you added your event listener.
Your way only puts events on elements already present on the page (they are in DOM structure). Any new elements will not have this event listener tied to them.
You can make a quick check for this by putting your code in function like
function setListeners() {
svg.select(".bars").selectAll("rect").on("mouseover", function() {
d3.select(this)
.transition()
.attr("fill", "red");
})
}
After you add any new bars on the screen, add this function call and see if it works for all elements. If this is indeed the case, you need to write your event listener in a way it works for all elements, including dynamically added ones. The way to do that is to set the event to some of the parent DOM nodes and then checking if you are hovering on exactly the thing you want the event to fire.
Example:
$(document).on(EVENT, SELECTOR, function(){
code;
});
This will put the event on the body and you can check then selector after it's triggered if you are over correct element. However it was a while since I worked with D3 and I'm not sure how D3, SVG and jQuery play together, last time I was doing it they had some troubles.
In this case event should be mouseover, selector should be your rect bars and function should be what ever you want to run.
If everything else fails in case they won't cooperate, just use the function to set event listeners and call it every time after you dynamically add new elements.
var data1 = [150,350,550]
var data2 = [100,300,500]
var sampleSVG = d3.select("body")
.append("svg")
.attr("width", 800)
.attr("height", 800);
var circles1 = sampleSVG
.append("g")
.attr("class", "circles1")
.selectAll(".circle1")
.data(data1)
.enter()
.append("circle")
.attr("class", "circle1")
.on("mousedown", animateFirstStep);
var circleAttributes1 = circles1
.attr("cx", function (d) { return d;})
.attr("cy", 200)
//.attr("class", function (d) { return "circle" + d;})
.attr("r", function(d) { return d/10;})
.style("fill", function(d){
var color;
if (d === 150){ color = "yellow";
} else if (d === 350) { color = "orange";
} else if (d === 550) { color = "red";
} return color;
})
function animateFirstStep(){
d3.selectAll(...??...)
.data(data1,function(d, i) { return d; })
.transition()
.delay(0)
.duration(2000)
.attr("r", 400)
.style("opacity", 0)
.each("end", animateSecondStep);
};
I have 3 circles and i want to click on one of them. When I click on one I want that one to grow bigger and disappear. the other 2 circles should also disappear but should NOT grow any bigger. Right now I name the class of each circle simply "circle1". But is also made a option(which are commented out) that gives each circle its own class based on the data. I have a function which animate the circles. But I don't know how to select a specific circle with a mouseclick and let that one grow bigger and disappear while the others don't grow but simply disappear. Can anyone help me out please??
You're on the right track, but instead of selecting elements by their class in the transition, I'd just bind the onclick event to each circle using the .on("click", ...) operator. You will then have access to each individual circle using the d3.select(this). Here's an example of what you can do with the circles1.on("click", ...) function (here I'm choosing how to animate the circles by their index i in the original data, but you can also filter by the value of d):
.on("click", function(d, i){
if (i == 0){
d3.select(this).transition()
.delay(0)
.duration(2000)
.attr("r", d)
.style("opacity", 0);
}
else{
d3.select(this)
.transition()
.delay(0)
.duration(2000)
.style("opacity", 0);
}
});
Complete working JSfiddle here.
Late to the party, but I think this is what you want: Fiddle
To "remember" the selected circle and the unselected circles, you need something like the following:
var grow;
var disappear;
Then modifying #mdml's answer a bit:
.on("click", function (d, i) {
// This is an assumption, I thought you wanted to remember
// so that you can toggle those states.
if (grow && disappear) {
disappear.transition()
.delay(0)
.duration(2000)
.style("opacity", 1);
grow.transition()
.delay(0)
.duration(2000)
.style("opacity", 1)
.attr("r", d / 10);
grow = null;
disappear = null;
} else {
var g = d3.selectAll("circle");
disappear = g.filter(function (v, j, a) {
return i !== j;
});
grow = g.filter(function (v, j, a) {
return i === j;
});
disappear.transition()
.delay(0)
.duration(2000)
.style("opacity", 0);
grow.transition()
.delay(0)
.duration(2000)
.attr("r", d)
.style("opacity", 0);
}
});
As you explained in the comments in the other answer, you wanted to select a circle and have that circle grow AND disappear. The other two circles will fade away. You also wanted to remember which was selected and which were not.
The Fiddle demo enables you to click on a circle, it will grow AND disappear, the others will fade. Click on it again and it will return to normal size, while the others will reappear.
I am trying to make tooltip like: http://jsfiddle.net/6cJ5c/10/ for my graph and that is the result on my realtime graph: http://jsfiddle.net/QBDGB/52/ I am wondering why there is a gap between the circles and the graph and why at the beginning there is a vertical line of circles? When it starts the circles are close to the curve but suddendly they start to jump up and down !! I want the circles to move smooothly and stick on the surface of the curve. I think the problem is that they are not moving with the "path1" and so it does not recognize the circles and thats why they are moving separetly or maybe the value of tooltipis are different of the value of the curve so they do not overlap!. That is how the data is generated ( value and time) and the tooltip:
var data1 = initialise();
var data1s = data1;
function initialise() {
var arr = [];
for (var i = 0; i < n; i++) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
arr.push(obj);
}
return arr;
}
// push a new element on to the given array
function updateData(a) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
a.push(obj);
}
var formatTime = d3.time.format("%H:%M:%S");
//tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var blueCircles = svg.selectAll("dot")
.data(data1s)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); })
.style("fill", "white")
.style("stroke", "red")
.style("stroke-width", "2px")
.on("mousemove", function(d ,i) {
div.transition()
.duration(650)
.style("opacity", .9);
div.html(formatTime(new Date(d.time)) + "<br/>" + d.value)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d ,i ) {
div.transition()
.duration(650)
.style("opacity", 0);
});
blueCircles.data(data1s)
.transition()
.duration(650)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); });
Please kindly tell me your opinions since I really need it :(
As I said maybe I should add "mouseover and mouse move functions" to the "path" to make it recognize the tooltip. something like following. but I am nor really sure :(
var path1 = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.data([data1])
.attr("class", "line1")
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);
I think your problem lies in the interpolation of your paths. You set the interpolation between points on your var area to "basis", which I found is a B-spline interpolation. This means the area drawn does not go through the points in your dataset, as shown in this example:
The path your points move over, though, are just straight lines between the points in your dataset. I updated and changed the interpolation from basic to linear, to demonstrate that it will work that way. I also set the ease() for the movement to linear, which makes it less 'jumpy'. http://jsfiddle.net/QBDGB/53/