I'm creating an area chart in D3 that includes two main features:
1) a time slider to change the date range on the x axis and update the chart (similar to a brush-to-zoom)
2) a tooltip that appears on hover to show the exact value at a given date
These two features are working when the chart loads, but when I change the x axis range by dragging the slide I encounter a problem: the area chart transitions as it should by zooming in with a new axis and area, but when I hover to reveal the tooltip, the tooltip location and values do not match the new path of the line/area.
I have tried wrapping all the code that creates the tooltip objects (appended to focus var) in a function, and then recalling that function when the slider is moved. I have also tried storing the x scale in a variable that is updated when the time slider is moved, and passing that to the drawFocus() function. The tooltip still appears in the original location.
Screenshot of where tooltip appears after zooming using time slider
Pasting link to bl.ock for full code below, but I think the problem is coming from something in the tipMove() function which is called whenever the user hovers over the chart. (current_x is storing the x scale defined by the time slider):
// function that adds tooltip on hover
function tipMove() {
// below code finds the date by bisecting and
// stores the x and y coordinate as variables
let x0 = current_x.invert(d3.mouse(this)[0]);
let i = bisectDate(data, x0, 1);
let d0 = data[i - 1];
let d1 = data[i];
let d = x0 - d0.date > d1.date - x0 ? d1 : d0;
// place the focus objects on the same path as the line
focus.attr('transform', `translate(${current_x(d.date)}, ${y(d.value)})`);
// position the x line
focus.select('line.x')
.attr('x1', 0)
.attr('x2', x(d.date))
.attr('y1', 0)
.attr('y2', 0);
// console.log(x0)
// position the y line
focus.select('line.y')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('y2', height - y(d.value));
// position the text
focus.select('text').text(d.value).transition() // slowly fade in the tooltip
.duration(100)
.style("opacity", 1);
// show the circle on the path
focus.selectAll('.focus circle')
.style("opacity", 1)
};
Full code here: https://blockbuilder.org/bendoesdata/99c2ca152f760b6d93c923ee9462f4fa
I expect the tooltip to follow the line after the chart is zoomed on, but instead the tooltip follows the original path of the line (which isn't visible within the clip path anymore).
Related
I am working on a d3 timeline chart -- but on load - I want the brush to be automatically deployed -- with the option of fine-tuning on a particular set of in/out dates
https://jsfiddle.net/nu1z4d3r/
https://jsfiddle.net/2y8gkas3/8/ -- latest example --
I've tried adding the -- draw brush logic to the bottom of the code base
https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81
what would be the correct values to make this work -- should xTop be x2?
function drawBrush(a, b) {
// define our brush extent
// define our brush extent
// note that x0 and x1 refer to the lower and upper bound of the brush extent
// while x2 refers to the scale for the second x-axis, for the context or brush area.
// unfortunate variable naming :-/
var x0 = xTop.invert(a*w)
var x1 = xTop.invert(b*w)
console.log("x0", x0)
console.log("x1", x1)
brush.extent([x0, x1])
// now draw the brush to match our extent
// use transition to slow it down so we can see what is happening
// set transition duration to 0 to draw right away
//brush(d3.select(".brush").transition().duration(500));
// now fire the brushstart, brushmove, and brushend events
// set transition the delay and duration to 0 to draw right away
//brush.event(d3.select(".brush").transition().delay(1000).duration(500));
}
// call drawBrush once on load with the default value
//var zoomA = d3.select("input#a")[0][0].value;
//var zoomB = d3.select("input#b")[0][0].value;
var zoomA = 0;
var zoomB = -1;
drawBrush(zoomA, zoomB);
/*
// update the extent and call drawBrush again
window.setTimeout(function() {
d3.select("input#a")[0][0].value = .2;
d3.select("input#b")[0][0].value = .7;
var zoomA = d3.select("input#a")[0][0].value;
var zoomB = d3.select("input#b")[0][0].value;
drawBrush(zoomA, zoomB)
}, 2500);
*/
With the brush -- there were some modifications I had to make
https://jsfiddle.net/m6ueL79o/3/
where the brush is called -- we append a variable to the artefact. We make a 2nd call with "brush.move, x1.range()" -- this loads the scrubber
var brush = d3.brushX()
.extent([[0, 0], [w, miniHeight]])
.on("brush", brushed);
var gBrush = mini.append("g")
.attr("class", "x brush")
.call(brush)
.call(brush.move, x1.range());
otherwise -- to load just the chart first -- do not have the .call(brush.move... and at the base add "drawBrush(timeBegin, timeEnd);"
I am using this example to create my own real-time graph using d3. In my version the graph is initialized with existing data. Problem is, the x-axis initialization causes a very small portion of the graph to show while it is transitioning or collapsing on the right before finally showing the normal scale and resultantly the normal graph. I am pretty sure the axis is causing it because the moment the axis returns to normal so does the graph. Is there a way to remove this transition at the begging or otherwise have it not skew the graph or not show until it is ready? Here is the problem in action, better than me trying to explain it: http://codepen.io/Dordan/pen/NbBjPB/
Here is the code snippet for creating the x-axis:
var limit = 60 * 1;
var duration = 750;
var now = new Date(Date.now() - duration);
var x = d3.time.scale().domain([now - (limit - 2), now - duration]).range([0, width]);
var axis = svg.append('g')
.attr('class', 'x-axis')
.attr('transform', 'translate(0,' + height + ')')
.call(x.axis = d3.svg.axis().scale(x).orient('bottom'));
The instantiation of your x scale is missing the '* duration' when you're calculating the domain. Use this instead and it works well:
var x = d3.time.scale().domain([now - (limit - 2) * duration, now - duration]).range([0, width]);
I'm using d3-tip in my visualisation. I now want to add tooltips to elements that are very wide and may extend out of the visible canvas. By default, the tooltip is shown in the horizontal center of an object, which means in my case that the tooltip might not be in the visible area. What I need is the tooltip showing up in the horizontal position of the cursor but I don't know how to change the tooltip position correctly. I can set an offset and I can get the coordinates of the cursor, but what I can't get is the initial position of the tooltip so that I can compute the right offset. Nor can I set an absolute position:
.on("mouseover",function(d){
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
tip.offset([-20,20]); // this works
tip.attr("x",40); // this doesn't
tip.show(d);
})
If you want to use offset, you can get the initial position of the tooltip after tip.show(d):
tip.style('top');
tip.style('left');
Similarly, to set the absolute position:
.on('mouseover', function(d){
var x = d3.event.x,
y = d3.event.y;
tip.show(d);
tip.style('top', y);
tip.style('left', x);
})
The previously stated answer did not work for me (and cannot be modified as "suggested edit queue is full.."), but with some minor adjustments, it is working fine:
.on('mouseover', function(d){
var x = d3.event.x,
y = d3.event.y;
tip.show(d);
tip.style('top', y-10 + 'px'); // edited
tip.style('left', x+'px'); // edited
})
I am building a chart using D3.js, which shows some info about employee's competencies.
screenshot:
As you can see, some text is larger than container element size, because of that, part of the text, is cut. I want to wrap these texts inside container.
I found this example , but I was not able to apply some solution to my chart.
Help would be appreciated...
Here is charts codepen url
and here is full screen view
p.s. I need text to be wrapped by words
In order to wrap the labels, you need to adjust Mike's solution to deal with textPath elements.
For this, we need several things:
1. Get the available width, reaching which the labels should wrap
You could compute the length of the arc itself, but I've done this by computing the segment created by the endpoints of your invisible paths that your labels follow. This will provide us with a little side margin as well, as the segment's length is shorter than the arc's length.
The distance between two points is computed as follows:
d = sqrt((x2 - x1)^2 + (y2 - y1)^2)
2. Wrap the labels when they rich available width and keep the aligned to center
For managing this one, I had to dig into the SVG documentation on the textPath element to see how it can be wrapped and shifted along the y axis.
Initially, I tried setting several textPath elements within one text label, but I couldn't manage to shift them along the y axis. It turns out, that for this you need to add tspan elements within textPath elements. But here another problem arose - I couldn't manage to keep them centrally aligned.
In the end, to achieve shift along y axis and central alignment, you need to use one textPath element (for horizontal alignment) with one tspan element inside (for vertical alignment).
3. Wrap the labels by letters, not by words
This is the point that I have assumed that you'll need namely letter wrapping (at the moment of writing, I didn't get the answer from OP), because on small sizes of your chart, there are words too long to fit into one line.
This was the easiest problem to solve. Just adjust the splitting and joining operations to switch from words to letters:
letters = text.text().split('').reverse(); // instead of .split(/\s+/)
...
tspan.text(line.join("")); // instead of .join(" ")
And here's the whole code that was changed, with relevant comments:
outerSvg.selectAll(".outerCircleText")
.data(pie(behaviorsDatasetOuterCircle))
.enter().append("text")
.attr("class", "outerCircleText")
//Move the labels below the arcs for those slices with an end angle greater than 90 degrees
.attr("dy", function (d, i) {
d.i = i;
return (d.startAngle >= 90 * Math.PI / 180 ? 18 : -11);
})
.text(function(d) { return d.data.name; })
.call(wrap); // Do not add `textPath` elements here. Instead, add them in the `wrap` function
function wrap(text) {
text.each(function() {
var text = d3.select(this),
letters = text.text().split('').reverse(),
letter,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
textPath = text.text(null).append("textPath") // Add a textPath element
.attr("startOffset", '50%')
.style("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#outerArc" + d.i; }),
tspan = textPath.append('tspan'), // Inslide textPath, add a tspan element, for offset feature later.
path = d3.select(text.select('textPath').attr('xlink:href')); // Get the path to compute width of text later.
var startLoc = /M(.*?)A/;
var newStart = path.attr('d').match(startLoc)[1];
var newEnd = path.attr('d').indexOf(' 0 0 0 ') > -1
? path.attr('d').split(' 0 0 0 ')[1]
: path.attr('d').split(' 0 0 1 ')[1] ;
// Compute the start/end coordinate points of the arc that the text will follow.
var x1 = parseFloat(newStart.split(' ')[0]),
y1 = parseFloat(newStart.split(' ')[1]),
x2 = parseFloat(newEnd.split(' ')[0]),
y2 = parseFloat(newEnd.split(' ')[1]);
// Compute the length of the segment between the arc start/end points. This will be the
// width which the labels should wrap when reaching it.
var width = Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
// And then we go on (with slight changes) with the example from Mike Bostock
// from here https://bl.ocks.org/mbostock/7555321
while (letter = letters.pop()) {
line.push(letter);
tspan.text(line.join(""));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(""));
line = [letter];
// Instead of adding only a tspan element, add a new textPath so that the wrapped
// letters will be aligned to center. Without it, the letters will start drawing
// from right with part of them invisible, like if the labels are not wrapped.
textPath = text.append("textPath")
.attr("startOffset", '50%')
.style("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#outerArc" + d.i; }),
// Add a tspan element to offset the wrapped letters from the previous line
tspan = textPath.append("tspan")
.attr('dy', '1em')
.attr('text-anchor', 'middle')
.text(letter);
}
}
});
}
In the end, it was an interesting challenge. Here is a fork of your codepen with a working example (the changes are starting with line 749).
The codepen has only the outer labels wrapped. I have left the inner labels for you to implement the approach described here. Good luck with that!
I will put a text element in every arc of my Pie Chart (center) - as shown in this example:
http://bl.ocks.org/mbostock/3887235
But I will only put the text element if the room is sufficient for the whole text, so im must compare the size of my text element with the "available" space in every arc.
I think I can do this with getBBox() to get the text dimensions... but how can I get (and compare) the dimension of the available space in every arc.
thx...!
This question has been asked several times before.
The solutions I have suggested there is to rotate the label but it has never quite satisfied me. Part of it was the horrible font rendering done by some browsers and loss in legibility that brings and the weird flip when one label crosses over the 180° line. In some cases, the results were acceptable and unavoidable, e.g. when the labels were too long.
One of the other solution, the one suggested by Lars, is to put the labels outside the pie chart. However, that just pushes the labels outside, granting them a larger radius, but does not solve the overlap problem completely.
The other solution is actually using the technique you suggest: just remove the labels which do not fit.
Hide overflowing labels
Compare Original, which has >= 65 label overflowing to Solution where the overflowing label is gone.
Reducing the problem
The key insight is to see that this problem is of finding whether one convex polygon (a rectangle, the bounding box) is contained inside another convex polygon(-ish) (a wedge).
The problem can be reduced to finding whether all the points of the rectangle lie inside the wedge or not. If they do, then the rectangle lies inside the arc.
Does a point lie inside a wedge
Now that part is easy. All one needs to do is to check:
The distance of the point from the center is less than the radius
The angle subtended by the point on the center is between the startAngle and endAngle of the arc.
function pointIsInArc(pt, ptData, d3Arc) {
// Center of the arc is assumed to be 0,0
// (pt.x, pt.y) are assumed to be relative to the center
var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
r2 = d3Arc.outerRadius()(ptData),
theta1 = d3Arc.startAngle()(ptData),
theta2 = d3Arc.endAngle()(ptData);
var dist = pt.x * pt.x + pt.y * pt.y,
angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system.
angle = (angle < 0) ? (angle + Math.PI * 2) : angle;
return (r1 * r1 <= dist) && (dist <= r2 * r2) &&
(theta1 <= angle) && (angle <= theta2);
}
Find the bounding box of the labels
Now that we have that out of the way, the second part is figuring out what are the four corners of the rectangle. That, also, is easy:
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; })
.each(function (d) {
var bb = this.getBBox(),
center = arc.centroid(d);
var topLeft = {
x : center[0] + bb.x,
y : center[1] + bb.y
};
var topRight = {
x : topLeft.x + bb.width,
y : topLeft.y
};
var bottomLeft = {
x : topLeft.x,
y : topLeft.y + bb.height
};
var bottomRight = {
x : topLeft.x + bb.width,
y : topLeft.y + bb.height
};
d.visible = pointIsInArc(topLeft, d, arc) &&
pointIsInArc(topRight, d, arc) &&
pointIsInArc(bottomLeft, d, arc) &&
pointIsInArc(bottomRight, d, arc);
})
.style('display', function (d) { return d.visible ? null : "none"; });
The pith of the solution is in the each function. We first place the text at the right place so that the DOM renders it. Then we use the getBBox() method to get the bounding box of the text in the user space. A new user space is created by any element which has a transform attribute set on it. That element, in our case, is the text box itself. So the bounding box returned is relative to the center of the text, as we have set the text-anchor to be middle.
The position of the text relative to the arc can be calculated since we have applied the transformation 'translate(' + arc.centroid(d) + ')' to it. Once we have the center, we just calculate the topLeft, topRight, bottomLeft and bottomRight points from it and see whether they all lie inside the wedge.
Finally, we determine if all the points lie inside the wedge and if they do not fit, set the display CSS property to none.
Working demo
Original
Solution
Note
I am using the innerRadius which, if non zero, makes the wedge non-convex which will make the calculations much more complex! However, I think the danger here is not significant since the only case it might fail is this, and, frankly, I don't think it'll happen often (I had trouble finding this counter example):
x and y are flipped and y has a negative sign while calculating Math.atan2. This is because of the difference between how Math.atan2 and d3.svg.arc view the coordinate system and the direction of positive y with svg.
Coordinate system for Math.atan2
θ = Math.atan2(y, x) = Math.atan2(-svg.y, x)
Coordinate system for d3.svg.arc
θ = Math.atan2(x, y) = Math.atan2(x, -svg.y)
You can't really do this with the bounding box because the bounding box is much larger than a wedge for the pie chart wedges. That is, even though the wedge at the outer edge would be wide enough to accommodate the text, that doesn't mean that it's wide enough at the actual position of the text.
Unfortunately, there's no easy way of doing what you're trying to do (pixel-level overlap testing). See e.g. this question for some more information. I would suggest simply putting the text labels outside of the pie chart so you don't run into this problem.