Is any possible to custom split tooltip position? - javascript

Expect
shared series have independent position tooltip
Tried
According to the Highcharts document, I know the Highcharts.tooltip.positioner can make some custom to the tooltip, but while I search around, all of them are un-split tooltip. and failed in the split situation, so Is there any possible to custom split tooltip position?
http://jsfiddle.net/TabGre/fyxqsq4L/
UPD:
each point's tooltip just above it, just like positioner:
positioner: function () {
return {
x: this.plotX;
y: this.plotY + 100;
}
}

As I mentioned in the comment, currently a positioner callback does not affect a split tooltip. However, it is possible to overwrite the function which is responsible for rendering a split tooltip. It requires calculating position on your own.
If you want a split tooltip to be in the top left corner as in your fiddle
you need to overwrite Highcharts.Tooltip.prototype.renderSplit = function(labels, points) with the desired position for each box.
var yPos = 0;
each(boxes, function(box, i) {
var point = box.point,
series = point.series;
// Put the label in place
box.tt.attr({
// visibility: box.pos === undefined ? 'hidden' : 'inherit',
x: (rightAligned || point.isHeader ?
//box.x :
0 :
point.plotX + chart.plotLeft + pick(options.distance, 16)),
// y: box.pos + chart.plotTop,
y: yPos,
anchorX: point.isHeader ?
point.plotX + chart.plotLeft : point.plotX + series.xAxis.pos,
anchorY: point.isHeader ?
box.pos + chart.plotTop - 15 : point.plotY + series.yAxis.pos
});
yPos += box.size;
});
example: http://jsfiddle.net/tbguemvL/

Related

Is it possible to access the exact xaxis position of the cursor in a highcharts tooltip?

I'm working on a highcharts solution which includes several different graph types in one single chart. Is it possible to have the exact time of mouse position displayed in the tooltip instead of a calculated range? (We're using highstock together with the boost and xrange module)
Also i need to always show the newest value of a series left from cursor position. Due to having an xRange Series i needed to refresh the tooltip with my own implementation since stickytracking doesnt work with xrange.
But right now the display of the green temperature series constantly switches from the actual value to the first value in the series (e.g when hovering around March 11th it constantly changes from 19.85°C back to 19.68°C which is the very first entry)
So i'm having 2 issues:
displaying the exact time in tooltip
displaying specific values in tooltip
I guess both could be solved with having the exact x position of cursor and for displaying the values i guess i did somewhat the right thing already with refreshing the tooltip on mousemove. Still the values won't always display properly.
I understand that Highcharts makes a best guess on the tooltip time value by displaying the range but to me it seems like it orients itself around the xRange Series.
I already tries to tinker around with the plotoptions.series.stickyTracking and tooltip.snap values but this doesn't really help me at all.
I understand too that this.x in tooltip formatter function will be bound to the closest point. Still i need it to use the current mouse position.
In a first attempt i was filtering through the series in the tooltip formatter itself before i changed to calculating the points on the mousemove event. But there i also couldn't get the right values since x was a rough estimate anyways.
Is there any solution to that?
At the moment i'm using following function onMouseMove to refresh the tooltip:
chart.container.addEventListener('mousemove', function(e) {
const xValue = chart.xAxis[0].toValue(chart.pointer.normalize(e).chartX);
const points = [];
chart.series.filter(s => s.type === "xrange").forEach(s => {
s.points.forEach(p => {
const { x, x2 } = p;
if (xValue >= x && xValue <= x2) points.push(p);
})
})
chart.series.filter(s => s.type !== "xrange").forEach(s => {
const point = s.points.reverse().find(p => p.x <= xValue);
if(point) points.push(point);
})
if (points.length) chart.tooltip.refresh(points, chart.pointer.normalize(e));
})
also i'm using this tooltip configuration and formatter:
tooltip: {
shared: true,
followPointer: true,
backgroundColor: "#FFF",
borderColor: "#AAAAAA",
borderRadius: 5,
shadow: false,
useHTML: true,
formatter: function(){
const header = createHeader(this.x)
return `
<table>
${header}
</table>
`
}
},
const createHeader = x => {
const headerFormat = '%d.%m.%Y, %H:%M Uhr';
const dateWithOffSet = x - new Date(x).getTimezoneOffset() * 60 * 1000;
return `<tr><th colspan="2" style="text-align: left;">${Highcharts.dateFormat(headerFormat, dateWithOffSet)}</th></tr>`
}
See following jsFiddle for my current state (just remove formatter function to see the second issue in action): jsFiddle
(including the boost module throws a script error in jsFiddle. Don't know if this is important so i disabled it for now)
finally found a solution to have access to mouse position in my tooltip:
extending Highcharts with a module (kudos to Torstein Hønsi):
(function(H) {
H.Tooltip.prototype.getAnchor = function(points, mouseEvent) {
var ret,
chart = this.chart,
inverted = chart.inverted,
plotTop = chart.plotTop,
plotLeft = chart.plotLeft,
plotX = 0,
plotY = 0,
yAxis,
xAxis;
points = H.splat(points);
// Pie uses a special tooltipPos
ret = points[0].tooltipPos;
// When tooltip follows mouse, relate the position to the mouse
if (this.followPointer && mouseEvent) {
if (mouseEvent.chartX === undefined) {
mouseEvent = chart.pointer.normalize(mouseEvent);
}
ret = [
mouseEvent.chartX - chart.plotLeft,
mouseEvent.chartY - plotTop
];
}
// When shared, use the average position
if (!ret) {
H.each(points, function(point) {
yAxis = point.series.yAxis;
xAxis = point.series.xAxis;
plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
});
plotX /= points.length;
plotY /= points.length;
ret = [
inverted ? chart.plotWidth - plotY : plotX,
this.shared && !inverted && points.length > 1 && mouseEvent ?
mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
inverted ? chart.plotHeight - plotX : plotY
];
}
// Add your event to Tooltip instances
this.event = mouseEvent;
return H.map(ret, Math.round);
}
})(Highcharts)
http://jsfiddle.net/2h951hdj/
Also you can wrap dragStart on the pointer and get exactly mouse position, in this case when you click on the chart area you will have the mouse position on the x-axis.
(function(H) {
H.wrap(H.Pointer.prototype, 'dragStart', function(proceed, e) {
let chart = this.chart;
chart.mouseIsDown = e.type;
chart.cancelClick = false;
chart.mouseDownX = this.mouseDownX = e.chartX;
chart.mouseDownY = this.mouseDownY = e.chartY;
chart.isZoomedByDrag = true;
console.log(chart.mouseDownX);
});
}(Highcharts));
Highcharts.chart('container', {
chart: {
events: {
load: function() {
let chart = this,
tooltip = chart.tooltip;
console.log(tooltip);
}
}
},
series: [{
data: [2, 5, 2, 3, 6, 5]
}],
});
Live demo: https://jsfiddle.net/BlackLabel/1b8rf9hc/

Highcharts position tooltip at the middle of stacked bar

I've been trying to use the tooltip positioner to get the tooltip at the middle of each stacked bar instead of the right side but I've not been able to find any variable that could be use to calculate the x of the appropiate point.
tooltip: {
positioner: function (labelWidth, labelHeight,point) {
return {
x: point.plotX - this.chart.hoverPoint.pointWidth,
y: point.plotY + this.chart.plotTop - labelHeight
};
}
}
Here is a codepen that show how it fails on the last point:
http://jsfiddle.net/vw7ebd4k/1/
To calculate the tooltip position you can use point.h and labelWidth. Try something like that:
tooltip: {
positioner: function (labelWidth, labelHeight, point) {
return {
x: point.plotX - point.h/2 + labelWidth/2,
y: point.plotY
};
}
}
To remove the unnecessary line between tooltip and point you can use tooltip.shape property.
shape: 'rect'

how to modify the interaction area of an svg text element to match its bounding box?

When I create text elements with Snap, say, the letter "a", the surrounding region that captures click events is far larger than the bounding box, by a scaling factor of at least 10. This means that if I create a second text element "b" that is close but visually separated from "a", the interaction regions have a large intersection, so that which element get clicked is mostly determined by which element is in an earlier/later layer.
Is it possible to shrink the interaction region to match the bounding box as closely as possible, so that layering doesn't affect which element's click event is triggered?
Here's a jsfiddle example, where the font size is initially set to 0.1px, which is on the order of magnitude I'm working with. (The size of the interaction area relative to the bounding box seems to increase as font size decreases.) Note that since "b" is created after "a", the only way to trigger "a" is clicking almost at the left edge of the canvas. What I was hoping to do is have a series of elements like these with similar spacing between them.
var s = Snap("#svg");
var fontsize = 0.1;
var make_text_box = function(string, fontsize, color) {
var text = s.text(0, 0, string);
text.attr({
'font-size': fontsize + 'px'
});
text.click(
function() {
text.attr({
fill: color
});
}
);
var bbx = text.getBBox().x;
var bby = text.getBBox().y;
var bbw = text.getBBox().w;
var bbh = text.getBBox().h;
var strokewidth = fontsize / 10;
var bbox = s.rect(bbx, bby, bbw, bbh)
.attr({
stroke: "black",
strokeWidth: strokewidth,
fill: "none"
});
var text_and_box = s.g(text, bbox);
return {
g: text_and_box,
x: bbx,
y: bby
};
};
var a_box = make_text_box("a", fontsize, "red");
var b_box = make_text_box("b", fontsize, "limegreen");
b_box.g.transform("translate(" + (2 * fontsize) + ", 0)");
s.attr({
viewBox: (a_box.x - 4 * fontsize) + ", " + (a_box.y - 4 * fontsize) + ", " + (30 * fontsize) + ", " + (10 * fontsize)
});
<script src="https://cdn.jsdelivr.net/snap.svg/0.1.0/snap.svg-min.js"></script>
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>
I have a feeling you may need a bit of a hacky workaround for this, as I don't know of a way to do what you want. I think there's several bugs out there, or maybe it's unclear what should happen with small font-sizes. Someone like Robert may have a better idea though.
If you leave the font-size to be bigger, but then scale down the element, you may have more success. So something like...
var fontsize = 10;
var text = s.text(0, 0, string);
text.attr({
'font-size': fontsize + 'px',
'transform': 's0.05'
});
jsfiddle
That's about as small as I can get it and still be able to see it and click in it. You may want to play a bit with shape-rendering and text-rendering as well to make it a bit sharper.

Show D3 link text right-side up

I have built a D3 force directed visualization with text labels along the links. The one problem I'm running into is these labels appearing upside down when the links are to the left of their source node. Example here:
The code where I position the path and text looks like so:
var nodes = flatten(data);
var links = d3.layout.tree().links(nodes);
var path = vis.selectAll('path.link')
.data(links, function(d) {
return d.target.id;
});
path.enter().insert('svg:path')
.attr({
class: 'link',
id: function(d) {
return 'text-path-' + d.target.id;
},
'marker-end': 'url(#end)'
})
.style('stroke', '#ccc');
var linkText = vis.selectAll('g.link-text').data(links);
linkText.enter()
.append('text')
.append('textPath')
.attr('xlink:href', function(d) {
return '#text-path-' + d.target.id;
})
.style('text-anchor', 'middle')
.attr('startOffset', '50%')
.text(function(d) {return d.target.customerId});
I know I will need to somehow determine the current angle of each path and then set the text position accordingly, but I am not sure how to.
Here is a link to a block based on this issue: http://blockbuilder.org/MattDionis/5f966a5230079d9eb9f4
The answer below has got me about 90% of the way there. Here is what my original visualization looks like with text longer than a couple digit number:
...and here is what it looks like utilizing the tips in the below answer:
So while the text is now "right-side up", it no longer follows the arc.
The arcs you draw are such that their tangent in the middle is exactly the direction of the baseline of the text, AND it is also colinear with the vector that separates the two tree nodes.
We can use that to solve the problem.
A bit of math is needed. First, let's define a function that returns the angle of a vector v with respect to the horizontal axis:
function xAngle(v) {
return Math.atan(v.y/v.x) + (v.x < 0 ? Math.PI : 0);
}
Then, at each tick, let's rotate the text in place by minus the angle of its baseline. First, a few utility functions:
function isFiniteNumber(x) {
return typeof x === 'number' && (Math.abs(x) < Infinity);
}
function isVector(v) {
return isFiniteNumber(v.x) && isFiniteNumber(v.y);
}
and then, in your tick function, add
linkText.attr('transform', function (d) {
// Checks just in case, especially useful at the start of the sim
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of the text element
var box = this.getBBox();
var center = {
x: box.x + box.width/2,
y: box.y + box.height/2
};
// Get the tangent vector
var delta = {
x: d.target.x - d.source.x,
y: d.target.y - d.source.y
};
// Rotate about the center
return 'rotate('
+ (-180/Math.PI*xAngle(delta))
+ ' ' + center.x
+ ' ' + center.y
+ ')';
});
});
edit: added pic:
edit 2 With straight lines instead of curved arcs (simply <text> instead of <textPath> inside of <text>), you can replace the part of the tick function that concerns linkText with this:
linkText.attr('transform', function(d) {
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of this element
var box = this.getBBox();
var center = {
x: box.x + box.width / 2,
y: box.y + box.height / 2
};
// Get the direction of the link along the X axis
var dx = d.target.x - d.source.x;
// Flip the text if the link goes towards the left
return dx < 0
? ('rotate(180 '
+ center.x
+ ' ' + center.y
+ ')')
: '';
});
and this is what you get:
Notice how the text gets flipped as the link goes from pointing more to the right to pointing more to the left.
The problem with this is that the text ends up below the link. That can be fixed as follows:
linkText.attr('transform', function(d) {
if (!(isVector(d.source) && isVector(d.target))) {
return '';
}
// Get the geometric center of this element
var box = this.getBBox();
var center = {
x: box.x + box.width / 2,
y: box.y + box.height / 2
};
// Get the vector of the link
var delta = {
x: d.target.x - d.source.x,
y: d.target.y - d.source.y
};
// Get a unitary vector orthogonal to delta
var norm = Math.sqrt(delta.x * delta.x + delta.y * delta.y);
var orth = {
x: delta.y/norm,
y: -delta.x/norm
};
// Replace this with your ACTUAL font size
var fontSize = 14;
// Flip the text and translate it beyond the link line
// if the link goes towards the left
return delta.x < 0
? ('rotate(180 '
+ center.x
+ ' ' + center.y
+ ') translate('
+ (orth.x * fontSize) + ' '
+ (orth.y * fontSize) + ')')
: '';
});
and now the result looks like this:
As you can see, the text sits nicely on top of the line, even when the link points towards the left.
Finally, in order to solve the problem while keeping the arcs AND having the text right side up curved along the arc, I reckon you would need to build two <textPath> elements. One for going from source to target, and one for going the opposite way. You would use the first one when the link goes towards the right (delta.x >= 0) and the second one when the link goes towards the left (delta.x < 0) and I think the result would look nicer and the code would not be necessarily more complicated than the original, just with a bit more logic added.

Calculate distance to move a box to remove intersection

I have two boxes that are overlapping and I want to figure out how to move one to remove the intersection.
I defined a vector from the center of one box to the other and move one box along that line, but that may be further than it needs to move, or may not be far enough. So I think what I need to do is figure out which corner of the stationary box lines closest to the line between the centers and then figure out how far along that line (or possibly projecting beyond that line) that corner would be. Then I can multiple my vector by that amount, but I'm having trouble wrapping my head around it.
Here's what I have at the moment, I'm adding items with an x,y,width and height property to a list and as I add each item, I'm checking for intersections with items already in the list. If an intersection is found, I try to move the new item and then try again:
function BoxList() {
var self = this;
var boxes = [];
self.add = function(item, iteration) {
// check intersections with existing boxes
iteration = iteration || 0;
if (iteration < 5) {
for (var i=0; i < boxes.length; i++) {
if (doesIntersect(getBounds(item),getBounds(boxes[i]))) {
item.elem.addClass("overlapped");
// Find vector from mid point of one box to the other
var centerA = { x: item.x + item.width / 2, y: item.y + item.height / 2 };
var centerB = { x: boxes[i].x + boxes[i].width / 2, y: boxes[i].y + boxes[i].height / 2 };
var line = { x1 : centerA.x, y1 : centerA.y, x2 : centerB.x, y2 : centerB.y };
var vector = { x : line.x1 - line.x2, y: line.y1 - line.y2 };
item.x = item.x + vector.x;
item.y = item.y + vector.y;
item.elem.offset({ left: item.x , top: item.y }); // TODO: calculate size of move needed
return self.add(item, iteration + 1);
}
}
}
boxes.push(item);
}
function getBounds(item) {
return { x1: item.x, x2: item.x + item.width, y1: item.y, y2: item.y + item.height };
}
function doesIntersect(a,b) {
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
}
}
Here's a simple fiddle
Click move to attempt to arrange the two boxes, note that the overlapping box is moved twice and gets moved further than it really needs to.
Any thoughts? Suggestions on better ways to approach this also greatly appreciated.
As I read it now you calculate the centers of both boxes and use those two points to make the vector that pushes one of the boxes. That's the wrong vector. If you place the boxes right on top of each other that vector will be (0,0). If the boxes only just clip each other the vector will be at it's highest possible value.
You can see this in action with the ghosts. First it gets pushed only a little bit, then it gets pushed a lot.
Instead the vector you need should be based on the size of the overlap. If the overlap is 20px by 30px your vector is (+20,+30)
var vector = {
x: Math.min(box1.x + box2.width, box2.x + box2.width) - Math.max(box1.x, box2.x),
y: Math.min(box1.y + box2.height, box2.y + box2.height) - Math.max(box1.y, box2.y)
}
vector.x is the top-right of the bounding box minus the bottom-left of the bounding box. Idem for vector.y.
This moves the box by exactly the right amount: http://jsfiddle.net/x8MT3/2/
I added a 3rd box pair that needs 2 iterations, probably the top box should move the other way. The vector as I've set it up is always (+,+), you can do your center point calculation to determine which sign each direction should have.

Categories

Resources