I have a linechart made with d3, but due to the shape of the data, the lines and dots (I'm using dot's over the lines for each specific data point) usually end up being in top of each other.
To counter this problem, I ended giving opacity 0.4 to the lines and dots, and when you hover over a line, the lines and dots of this specific line of data pops out, and sets it's opacity to 1.
My problem is: I'm using the .raise() funcion to make them pop out and stand over the rest of the lines and dots, the function is working only with my lines selection and not with my dots selection, and I don't know why.
My code:
// draw the data lines
const lines = svg.selectAll('.line')
.data(this.data)
.enter()
.append('path')
.attr('class', 'data.line')
.attr("fill", "none")
.attr("stroke", d => colors(d.key))
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 2.5)
.attr('stroke-opacity', 0.4)
.attr('d', d => line(d.values))
.on('mouseenter', d => {
// Highlight them
let myCircles = circles.selectAll('.circle');
lines.attr('stroke-opacity', b => {
return b.key === d.key ? 1 : 0.4;
});
myCircles.attr('fill-opacity', b => {
return b[this.typeIdentifier] === d.key ? 1 : 0.4;
});
// Bring them to the front
myCircles = circles.selectAll('.circle')
.filter(b => b[this.typeIdentifier] === d.key);
const myLines = lines.filter(b => b.key === d.key);
myLines.raise();
myCircles.raise();
});
// draw the circles
const circles = svg.selectAll('.circle')
.data(this.data)
.enter()
.append('g');
circles.selectAll('.circle')
.data(d => d.values)
.enter()
.append('circle')
.attr('class', 'circle')
.attr('stroke', 'white')
.attr('stroke-width', 1)
.attr('r', 6)
.attr('fill', d => colors(d[this.typeIdentifier]))
.attr('fill-opacity', 0.4)
.attr('cx', d => x(d[this.xAxisValue]) + x.bandwidth() / 2)
.attr('cy', d => y(d[this.yAxisValue]))
.on('mouseenter', (d, b, j) => {
tooltip.raise();
tooltip.style("display", null);
tooltip.select("#text1").text(d[this.typeIdentifier])
.attr('fill', colors(d[this.typeIdentifier]));
tooltip.select('#text4').text(d[this.yAxisValue]);
tooltip.select('#text5').text(d[this.xAxisValue]);
const tWidth = tooltip.select('#text1').node().getComputedTextLength() > 60 ? tooltip.select('#text1').node().getComputedTextLength() + 20 : 80;
tooltipRect.attr('width', tWidth);
const xPosition = d3.mouse(j[b])[0];
const yPosition = d3.mouse(j[b])[1];
if (xPosition + tWidth + 35 < this.xWIDTH) { // display on the right
tooltip.attr("transform", `translate(${xPosition + 15}, ${yPosition - 25})`);
} else { // display on the left
tooltip.attr("transform", `translate(${xPosition - tWidth - 15}, ${yPosition - 25})`);
}
})
.on('mouseleave', d => {
tooltip.style("display", "none");
})
So, when you hover the mouse over a line, this should bring the line and dots associated to it to the front, with opacity 1, but for some reason, it's only working on the lines selection, and not on the myCircles selection. The selection is not empty, and I've been printing them all along to test it out. Also, I've tried to bring the circles one by one (with singular selections, and with raw elements) to the front using the .raise() method, and it's not working eiter.
Why is it not working? Could it have to do with the tooltip on hover over the circles? Am I doing something wrong and not seeing it?
Actually, selection.raise() is working. The problem here is just the tree structure of your SVG: all the circles for a given line belong to a <g> element.
If you look at the docs, you'll see that selection.raise():
Re-inserts each selected element, in order, as the last child of its parent.
The emphasis above is mine: the key work here is parent. So, what you want is to raise the <g> element that contains the selected circles above the other <g> elements for the other circles, not the circles inside their <g> parent.
In your case, it's as simple as changing...
myCircles = circles.selectAll('.circle').filter(etc...)
...to:
myCircles = circles.filter(etc...)
Now, myCircles is the selection with the <g> element, which you can raise. Pay attention to the filter function: as you didn't share your data structure I don't know if the data array for the <g> elements (that is, this.data) contains the key property. Change it accordingly.
Here is a demo:
We have a set of circles for each line, each set inside their own <g> parent. Only the left circles are separated, all other circles are draw one over the other on purpose. When you hover over a circle (use the ones on the left) its <g> container is raised, in this case using...
d3.select(this.parentNode).raise()
..., so all circles are visible:
const svg = d3.select("svg");
const scale = d3.scaleOrdinal(d3.schemeSet1);
const lineGenerator = d3.line()
.x(function(d) {
return d.x
})
.y(function(d) {
return d.y
})
const data = d3.range(5).map(function(d) {
return {
key: d,
values: d3.range(5).map(function(e) {
return {
x: 50 + 100 * e,
y: e ? 150 : 50 + 50 * d
}
})
}
});
const lines = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.attr("d", function(d) {
return lineGenerator(d.values);
})
.style("fill", "none")
.style("stroke-width", "3px")
.style("stroke", function(d) {
return scale(d.key)
});
const circleGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g");
const circles = circleGroups.selectAll(null)
.data(function(d) {
return d.values
})
.enter()
.append("circle")
.attr("r", 20)
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.style("fill", function(d) {
return scale(d3.select(this.parentNode).datum().key)
});
circles.on("mouseover", function(d) {
const thisKey = d3.select(this.parentNode).datum().key;
lines.filter(function(e) {
return e.key === thisKey;
}).raise();
d3.select(this.parentNode).raise();
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>
Related
JSFiddle example
I've noticed that when updating positions of svg elements in a d3-force diagram, updating the positions of elements using (in the case of circles) the cx and cy attributes is much smoother than using the transform attribute.
In the example JSFiddle, there are two separate force simulations side-by-side. The one on the left updates positions using the transform attribute:
sim_transform.on('tick', function () {
circles_transform.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
The one on the right updates positions using the cx and cy attributes of a circle:
sim_position.on('tick', function () {
circles_position
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
});
The simulations appear identical until they're just about to become static, at which point the one using transforms starts to jitter quite a bit. Any ideas what is causing this? Can it be fixed so that the animation remains smooth using transforms?
It seems to me that the issue you're observing (only reproducible in FireFox, as #altocumulus noted) has something to do with the way FF uses floating numbers for the translate of the transform attribute.
We can see this if we set both simulations to use integers, doing ~~(d.x) and ~~(d.y). Have a look, both will jitter:
var svg = d3.select('svg');
var graph_transform = gen_data();
var graph_position = gen_data();
var force_left = d3.forceCenter(
parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var force_right = d3.forceCenter(
2 * parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var sim_transform = d3.forceSimulation()
.force('left', force_left)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var sim_position = d3.forceSimulation()
.force('right', force_right)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var g_transform = svg.append('g');
var g_position = svg.append('g');
var circles_transform = g_transform.selectAll('circle')
.data(graph_transform.nodes)
.enter()
.append('circle')
.attr('r', 40);
var circles_position = g_position.selectAll('circle')
.data(graph_position.nodes)
.enter()
.append('circle')
.attr('r', 40);
sim_transform
.nodes(graph_transform.nodes)
.force('link')
.links(graph_transform.links);
sim_position
.nodes(graph_position.nodes)
.force('link')
.links(graph_position.links);
sim_transform.on('tick', function() {
circles_transform.attr('transform', function(d) {
return 'translate(' + (~~(d.x)) + ',' + (~~(d.y)) + ')';
});
});
sim_position.on('tick', function() {
circles_position
.attr('cx', function(d) {
return ~~d.x;
})
.attr('cy', function(d) {
return ~~d.y;
})
});
function id(d) {
return d.id;
}
function gen_data() {
var nodes = [{
id: 'a'
},
{
id: 'b'
},
{
id: 'c'
},
{
id: 'd'
}
]
var links = [{
source: 'a',
target: 'b'
},
{
source: 'b',
target: 'c'
},
{
source: 'c',
target: 'd'
},
{
source: 'd',
target: 'a'
}
];
return {
nodes: nodes,
links: links
}
}
svg {
width: 100%;
height: 500px;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, in your original code, it seems like the circles move correctly when using cx and cy, but they jump from integer to integer when using translate (or maybe half pixel, see the last demo). If the hypothesis here is correct, the reason that you just see the effect when the simulation is cooling down is because, at that moment, the movements are smaller.
Demos
Now, if we get rid of the simulation, we can see that this strange behaviour also happens with a very basic transform. To check this, I created a transition for a big black circle, using a linear ease and a very long time (to facilitate seeing the issue). The circle will move 30px to the right. I also put a gridline to make the jumps more noticeable.
(Warning: the demos below are only reproducible in FireFox, you won't see any difference in Chrome/Safari)
If we use cx, the transition is smooth:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("cx", "230")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
However, if we use translate, you can see the circle jumping 1px at every move:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("transform", "translate(30,0)")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
For you people running this in Chrome/Safari, this is how the last snippet looks like in Firefox. It's like the circle is being moved half a pixel at every change... definitely not as smooth as changing cx:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98);
var timer = d3.timer(function(t){
if(t>10000) timer.stop();
circle.attr("cx", 200 + (~~(60/(10000/t))/2));
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
As this is an implementation issue only visible in FF, it may be worth reporting a bug.
I am using d3.js with the force layout. Now, with the help of the dynamically changing array data it is possible to highlight nodes dynamically based on the array. Also with the code below i am able to show up dynamically the names of the nodes, which are part of the array, as a text.
So, when the array has for example 3 entries, then 3 nodes are shown up and also 3 names of the nodes appear. Let's say their names are "a", "b", "c", so the text "a", "b", "c" appears on screen.
Now, when i click on the new appeared text "a", then i want the node, which contains that name, to be filled green. I tried this with the function called specialfunction. The problem is, that all nodes fill green when i click
on the text "a". Can someone of you guys maybe help? Thanks.
var texts = svg.selectAll(".texts")
.data(data);
textsExit = texts.exit().remove();
textsEnter = texts.enter()
.append("text")
.attr("class", "texts");
textsUpdate = texts.merge(textsEnter)
.attr("x", 10)
.attr("y", (d, i) => i * 16)
.text(d => d.name)
.on("click", specialfunction);
function specialfunction(d) {
node.style("fill", function(d){ return this.style.fill = 'green';});
};
Right now, your specialFunction function is only taking the nodes selection and setting the style of all its elements to the returned value of...
this.style.fill = 'green';
... which is, guess what, "green".
Instead of that, filter the nodes according to the clicked text:
function specialFunction(d) {
nodes.filter(function(e) {
return e === d
}).style("fill", "forestgreen")
}
In this simple demo, d is the number for both texts and circles. Just change d in my demo to d.name or any other property you want. Click the text and the correspondent circle will change colour:
var svg = d3.select("svg");
var data = d3.range(5);
var nodes = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 30 + d * 45
})
.attr("r", 20)
.style("fill", "lightblue")
.attr("stroke", "gray");
var texts = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", 88)
.attr("x", function(d) {
return 26 + d * 45
})
.attr("fill", "dimgray")
.attr("cursor", "pointer")
.text(function(d) {
return d
})
.on("click", specialFunction);
function specialFunction(d) {
nodes.filter(function(e) {
return e === d
}).style("fill", "forestgreen")
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
EDIT: Answering your comment, this even simpler function can set the circles to the original colour:
function specialFunction(d) {
nodes.style("fill", function(e){
return e === d ? "forestgreen" : "lightblue";
})
}
Here is the demo:
var svg = d3.select("svg");
var data = d3.range(5);
var nodes = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 30 + d * 45
})
.attr("r", 20)
.style("fill", "lightblue")
.attr("stroke", "gray");
var texts = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", 88)
.attr("x", function(d) {
return 26 + d * 45
})
.attr("fill", "dimgray")
.attr("cursor", "pointer")
.text(function(d) {
return d
})
.on("click", specialFunction);
function specialFunction(d) {
nodes.style("fill", function(e){
return e === d ? "forestgreen" : "lightblue";
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I have this piece of code in which circles are drawn, I need to put a text inside each circle, I would also like to know how I can put a certain size to each of the elements of the circle.
Thank you very much.
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function (d) { return d.id; });
// Create new circle elements each with class `bubble`.
// There will be one circle.bubble for each object in the nodes array.
// Initially, their radius (r attribute) will be 0.
bubbles.enter().append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('fill', function (d) { return fillColor(d.group); })
.attr('stroke', function (d) { return d3.rgb(fillColor(d.group)).darker(); })
.attr('stroke-width', 2)
.on('mouseover', showDetail)
.on('mouseout', hideDetail);
// Fancy transition to make bubbles appear, ending with the
// correct radius
bubbles.transition()
.duration(2000)
.attr('r', function (d) { return d.radius; });
A good practice would be to create a group element for each bubble because they will be composed of two elements - a circle and text.
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function(d) {
return d.id;
})
.enter()
.append('g')
.attr("transform", d => `translate(${d.x}, ${d.y})`)
.classed('bubble', true)
.on('mouseover', showDetail)
.on('mouseout', hideDetail)
After that, circles and texts can be appended:
circles = bubbles.append('circle')
.attr('r', 0)
.attr('stroke-width', 2)
texts = bubbles.append('text')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.style('font-size', d => d.radius * 0.4 + 'px')
.attr('fill-opacity', 0)
.attr('fill', 'white')
.text(d => d.text)
// Fancy transition to make bubbles appear, ending with the
// correct radius
circles.transition()
.duration(2000)
.attr('r', function(d) {
return d.radius;
});
For hiding/showing text, you can use fill-opacity attribute and set it 0 when the text should be hidden, and 1 if it should be shown:
function showDetail(d, i) {
d3.select(this.childNodes[1]).attr('fill-opacity', 1)
}
function hideDetail(d, i) {
d3.select(this.childNodes[1]).attr('fill-opacity', 0)
}
example: https://jsfiddle.net/r880wm24/
I have d3 line graph that is constantly updated with new set of data. The issue is my line graph is drawn above the some rectangular blocks. On page load my line graph is always in the front of the rect but after the page is being refreshed the line graph is going behind the rectangular block. Can any of you help me fix this problem ?
My code is set up like this
function drawRect(SVG, cData, type) {
let selector = '.ca';
let className = 'c a';
let tcHeight = verticalSize + MIN_CELL_PADDING;
let getTranslateString = function (index) {
let yVal = columnHeight - ((index + 1) * tcHeight);
return `translate(${xVal}, ${yVal})`;
let rects = d3.select(columnSVG)
.selectAll(selector)
.data(cData.filter((d) => {
return d;
}));
rects.enter()
.append('g')
.attr('class', className)
.attr('transform', (d, ix) => {
return getTranslateString(ix);
})
.each(function () {
d3.select(this)
.append('rect')
.attr('width', cellSize)
.attr('height', verticalSize)
.attr('rx', 4)
.attr('ry', 4)
.attr('time', (d) => {
return cData.date;
})
.attr('fill', (d) => {
return changeColor(d);
});
});
rects.transition()
.attr('transform', (d, ix) => {
return getTranslateString(ix);
});
rects.each(function (d) {
let node = d3.select(this);
node.selectAll('rects').transition()
.attr('width', cellSize)
.attr('height', verticalSize)
.attr('rx', 4)
.attr('ry', 4)
}
function drawOline(aData, dData, time) {
let aLine = d3.svg.line()
.defined((d) => {
return !isNaN(d.Ptile);
})
.x((d) => {
return ptime(moment(d.day).utc());
})
.y((d) => {
return aY(d.Ptile);
});
let dLine = d3.svg.line()
.defined((d) => {
return !isNaN(d.Ptile);
})
.x((d) => {
return ptime(moment(d.day).utc());
})
.y((d) => {
return dY(d.Ptile);
});
if (aData && aData.length > 0) {
if (g.select('.aline')[0][0] == null) {
g.append('g')
.append('path')
.datum(aData)
.attr('class', 'line aline')
.attr('fill-opacity', 1.0)
.attr('d', aline);
} else {
g.select('.aline')
.datum(aData)
.transition()
.attr('fill-opacity', 1.0)
.attr('d', aline);
}
} else {
g.select('.aline')
.transition()
.attr('fill-opacity', 1.0)
.attr('d', aline);
}
if (dData && dData.length > 0) {
if (g.select('.dline')[0][0] == null) {
g.append('g')
.append('path')
.datum(dData)
.attr('class', 'line dline')
.attr('fill-opacity', 1.0)
.attr('d', dline);
} else {
g.select('.dline')
.datum(dData)
.transition()
.attr('fill-opacity', 1.0)
.attr('d', dline);
}
} else {
g.select('.dline')
.transition()
.attr('fill-opacity', 1.0)
.attr('d', dline);
}
}
The visual occlusion (hiding) of some SVG objects by others (e.g. lines by rects, or vice versa) is very dependent on their drawing order. Unlike HTML/CSS, SVG does not have a true z-index or "what's on top?" indicator.
The trick is often to draw the items you want to see on top last. That's not always convenient, however. For example, you may not way to redraw lines every time you redraw the blocks.
A way to preserve the visual ordering of objects, even when they're redrawn, is to put them into <g> groups. The ordering of the groups need not change, even if the items are updated. For example:
var rectsG = svg.append('g').attr('class', 'rects');
var linesG = svg.append('g').attr('class', 'lines');
Then instead of drawing into the global svg element, direct your appends to individual groups. They will act as layers:
linesG.append('line')
...more here...
rectsG.append('rect')
...more here...
Because the groups are ordered in the document top to bottom, it really doesn't matter what order you draw or redraw their constituent elements. The ordering of the <g> containers is what will determine visual occlusion.
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/