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.
Related
I am trying to add some text at the end of the bars of a d3js bar chart.
The bar chart has transition with a delay. The source code can be found here https://bl.ocks.org/deciob/ffd5c65629e43449246cb80a0af280c7.
Unfortunately, with my code below the text does not follow the bars and I am not sure what I am doing wrong.
I thought the append text should be placed in the drawBars function no?
function drawBars(el, data, t) {
let barsG = el.select('.bars-g')
if (barsG.empty()) {
barsG = el.append('g')
.attr('class', 'bars-g');
}
const bars = barsG
.selectAll('.bar')
.data(data, yAccessor);
bars.exit()
.remove();
bars.enter()
.append('rect')
.attr('class', d => d.geoCode === 'WLD' ? 'bar wld' : 'bar')
.attr('x', leftPadding)
.attr('fill', function (d) {return d.geoColor;})
bars.enter()
.append('text')
.attr('x', d => xScale(xAccessor(d)))
.attr('y', d => yScale(yAccessor(d)))
.text('Hello')
.merge(bars).transition(t)
.attr('y', d => yScale(yAccessor(d)))
.attr('width', d => xScale(xAccessor(d)))
.attr('height', yScale.bandwidth())
.delay(delay)
}
What I am trying to achieve is for the text to follow the bars (and also later for the text to be updated to another value).
Thanks for any kind of help.
Found the answer, for anyone wondering you need to create a new function (eg: drawText()) and call it in later just below where the drawBars() function is called:
function drawText(el, data, t) {
var labels = svg.selectAll('.label')
.data(data, yAccessor);
var new_labels = labels
.enter()
.append('text')
.attr('class', 'label')
.attr('opacity', 0)
.attr('y', d => yScale(yAccessor(d)))
.attr('fill', 'blue')
.attr('text-anchor', 'middle')
new_labels.merge(labels)
.transition(t)
.attr('opacity', 1)
.attr('x', d => xScale(xAccessor(d))+50)
.attr('y', d => yScale(yAccessor(d)))
.text(function(d) {
return d.value;
});
labels
.exit()
.transition(t)
.attr('y', height)
.attr('opacity', 0)
.remove();
}
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>
I have the following enter / update / exit phases defined.
// this.x = my x time scale
// this.y = my y scale
// this.c = a color scale with 2 colors (red,blue)
// this.chart = D3.select() element
let series = D3.stack().keys(['point', 'topPoint'])(<any[]>this.barData);
this.chart
.append('g')
.selectAll('g')
.data(series)
.enter().append('g')
.attr('class', (d) => {return d.key + ' layer';})
.attr('fill', (d) => {return this.c(d.key);})
.selectAll('.bar')
.data((d) => {return d;})
.enter()
.append('rect')
.attr('class', 'bar');
// Update Phase
this.chart.selectAll('.bar').transition()
.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
// Exit phase
this.chart.selectAll('.point.layer').selectAll('.bar').exit().remove();
this.chart.selectAll('.topPoint.layer').selectAll('.bar').exit().remove();
When the data changes, the new bars are drawn, but they are drawn over the old bars.
if you use d3 v4 try this:
let series = D3.stack().keys(['point', 'topPoint'])(<any[]>this.barData);
const elements = this.chart
.append('g')
.selectAll('g')
.data(series);
elements.enter().append('g')
.attr('class', (d) => {return d.key + ' layer';})
.attr('fill', (d) => {return this.c(d.key);})
.each(function(d){
d3.select(this)
.append('rect')
.attr('class', 'bar');
})
.merge(elements) // updatePhase
.each(function(d){
d3.select(this).select(".bar")
.transition()
.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
}
// Exit phase
elements.exit().remove();
So the problem was my selecting of the elements I wish to bind and unbind.
this.chart
.selectAll('.layer')
.data(series)
.enter()
.append('g')
.attr('class', (d) => {return d.key + ' layer';});
// Set the enter phase for the bars within the groups, with the data derived from the layer data binding
this.chart.selectAll('.layer')
.selectAll('.bar')
.data((d) => {return d;})
.enter()
.append('rect')
.attr('class', 'bar');
// Set the update phase for the layers to fill the groups with the relevant color
let layers = this.chart.selectAll('.layer').attr('fill', (d) => {return this.c(d.key);});
// Update Phase
let bars;
if(this.animate) {
// Set the update phase of the bar data based on the data derived from the layer update phase
bars = layers.selectAll('.bar').data((d) => {return d;}).transition();
} else {
bars = layers.selectAll('.bar').data((d) => {return d;});
}
// Set the update phase of the bar data based on the data derived from the layer update phase
bars.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
// Exit phase
this.chart.selectAll('.layer').data(series).exit().remove();
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/
My goal is to have an interactive chart which displays additional information on the arc next to the chart. The code below is not clean, but is functional and does (almost) everything I want it to.
Specifically, it draws a little arc-pie chart, when you hover over an arc it lightens the arc and draws a line from the center of that arc to the data area to the right where a spend and a description are listed.
HOWEVER, the code seems inelegant and the datapoint (text) it is displaying are all the same one. It doesn't seem to be adding the additional data elements in order to display them, or it IS but is only ever selecting the first to display!
The HTML, which I haven't bothered to copy in is just a blank web page with a single div: <div id="pie">1,2,3</div>
var format = d3.format(",.2f");
var data = [
{'spend': 15, 'description': 'Controlled'},
{'spend': 3, 'description': 'Data Usage'},
{'spend': 21, 'description': 'International Roaming'}
];
var kulor = d3.scale.ordinal()
.range(['#648631','#D84B4B','#A05AE0']);
var width = 160,
height = 80,
radius = height / 2 - 10, // how wide is the circle within the box?
innerRadius = radius - 10, // how thich is the donut (the -# is the thickness)
cornerRadius = 3, // how wide are the curves of each section
padAngle = .07, // distance between items (in angles)
startLine = [40, -25]; // where the description line starts
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(radius)
.cornerRadius(cornerRadius);
var pie = d3.layout.pie()
.padAngle(padAngle)
.value(function (d) {
return d.spend;
});
var arcs = d3.select('#pie').html('').append('svg')
.data([data])
.attr('width', width)
.attr('height', height)
.append('g')
// keep centered but left
.attr('transform', 'translate(' + height / 2 + ',' + height / 2 + ')');
arcs.selectAll('path')
.data(pie).enter()
.append('path')
.style('fill', function(d, i) { return kulor(i); })
.attr('d', arc);
arcs.selectAll('line')
.data(pie).enter()
.append('line')
.classed('slice', true)
.attr('lineNum', function(d,i){ return 'sl'+i; })
.attr('x1', function(d, i) {
return arc.centroid(d, i)[0];
})
.attr('y1', function(d, i) {
return arc.centroid(d, i)[1];
})
.attr('x2', startLine[0]).attr('y2', startLine[1])
.attr('stroke-width', 2).attr('stroke','#333')
.style('visibility', 'hidden');
// fixed line
arcs.append('line')
.classed('fixedLine', true)
.attr('x1', startLine[0]).attr('y1', startLine[1])
.attr('x2', 120).attr('y2', startLine[1])
.attr('stroke-width', 2).attr('stroke','#333')
.style('visibility', 'hidden');
// Spend
arcs.each(function(d, i) {
console.log(d[i].spend + " " + i);
arcs.append('text')
.classed('spend', true)
.text(function(d,i){
console.log(d);
return '$' + format(d[i].spend);
})
.attr('x', 80).attr('y', startLine[1] - 4)
.attr('font-size',12)
.attr('font-family','Helvetica, sans-serif')
.attr('text-anchor','middle')
.attr('font-weigt','bold')
.style('visibility', 'hidden');
})
// Spend Description
arcs.append('text')
.text(function(d,i){ return d[i].description; })
.attr('x', 80).attr('y', startLine[1] + 12)
.attr('font-size',12)
.attr('font-family','Helvetica, sans-serif')
.attr('text-anchor','middle')
.attr('font-weigt','bold')
.style('visibility', 'hidden');;
arcs.selectAll('path')
.on('mouseover', function(d, i) {
var index = i + 1;
var nodeSel = d3.select(this).style({opacity:'0.8'});
nodeSel.select('text').style({opacity:'1.0'});
d3.select('.slice:nth-of-type('+index+')').style('visibility', 'visible');
d3.select('.spend:nth-of-type('+index+')').style('visibility', 'visible');
d3.select('.fixedLine').style('visibility', 'visible');
d3.selectAll('text').style('visibility', 'visible');
})
.on('mouseout', function(d, i) {
var nodeSel = d3.select(this).style({opacity:'1.0'});
nodeSel.select('text').style({opacity:'0'});
arcs.selectAll('.slice').style('visibility', 'hidden');
arcs.selectAll('.fixedLine').style('visibility', 'hidden');
arcs.selectAll('text').style('visibility', 'hidden');
});
Problem1:
You don't have to add as many text as the number of arcs.
So this is wrong.
arcs.each(function(d, i) {
console.log(d[i].spend + " " + i);
arcs.append('text')
.classed('spend', true)
.text(function(d,i){
console.log(d);
return '$' + format(d[i].spend);
})
.attr('x', 80).attr('y', startLine[1] - 4)
.attr('font-size',12)
.attr('font-family','Helvetica, sans-serif')
.attr('text-anchor','middle')
.attr('font-weigt','bold')
.style('visibility', 'hidden');
})
The right way is just add one text DOM like this (and to that DOM give the description on mouse over)
arcs.append('text')
.classed('spend', true)
.attr('x', 80).attr('y', startLine[1] - 4)
.attr('font-size',12)
.attr('font-family','Helvetica, sans-serif')
.attr('text-anchor','middle')
.attr('font-weigt','bold')
.style('visibility', 'hidden');
Problem 2:
You don't need to set the text while making the text DOM like this:
.classed('spend', true)
.text(function(d,i){
console.log(d);
return '$' + format(d[i].spend);
})
You need to add the description on mouse over like this
.on('mouseover', function(d, i) {
var index = i + 1;
var nodeSel = d3.select(this).style({opacity:'0.8'});
nodeSel.select('text').style({opacity:'1.0'});
d3.select('.slice:nth-of-type('+index+')').style('visibility', 'visible');
d3.select('.spend:nth-of-type('+index+')').style('visibility', 'visible');
d3.select('.fixedLine').style('visibility', 'visible');
d3.selectAll('text').style('visibility', 'visible');
//setting the description data to the text dom
d3.selectAll(".description").text(d.data.description);
//setting the spend data to the text dom
d3.selectAll(".spend").text('$' + format(d.data.spend))
});
Working code here.