I'm trying to increase the hover detection radius for scatter points in HighCharts, but I've found no official way of doing it.
Essentially, I want each point to have an invisible radius that triggers the hover effect when the mouse enters it.
I know stickyTracking exists, but in my case it feels a bit too sticky.
Turning it off, however, means having to be extremely precise before the tooltip shows up.
StickyTracking off: https://jsfiddle.net/wsxkLn25
StickyTracking on: https://jsfiddle.net/wsxkLn25/1/
Is there a way to trigger the hover effect on a point before the mouse enters the actual radius of the point without stickyTracking?
There is no such feature in Highcharts, but you can implement it by enabling sticky tracking and process the internal getHoverData method only if a hovered point is close enough to a mouse cursor. Example:
(function(H) {
H.wrap(H.Pointer.prototype, 'getHoverData', function(proceed, existingHoverPoint, existingHoverSeries, series, isDirectTouch, shared, e) {
var hoverData = proceed.apply(this, Array.prototype.slice.call(arguments, 1));
var RADIUS = 20;
var point = hoverData.hoverPoint;
var chart = this.chart;
var plotX;
var plotY;
if (point) {
plotX = point.plotX + chart.plotLeft;
plotY = point.plotY + chart.plotTop;
if (
plotX + RADIUS > e.chartX && plotX - RADIUS < e.chartX &&
plotY + RADIUS > e.chartY && plotY - RADIUS < e.chartY
) {
return hoverData;
} else if (chart.hoverPoint) {
chart.hoverPoint.setState('');
chart.tooltip.hide();
}
}
return {
hoverPoint: null,
hoverPoints: [],
hoverSeries: null
};
});
}(Highcharts));
Live demo: https://jsfiddle.net/BlackLabel/sb35j0ov/
Docs: https://www.highcharts.com/docs/extending-highcharts/extending-highcharts
Related
I'm using the svg-pan-zoom code https://github.com/ariutta/svg-pan-zoom to make an svg map of some kind, now it is time to add a feature to pan & zoom to an art component of the svg on a click event. However, I'm not sure how to use the panBy() function to get to a desired svg art item: I tried to use the getBBox() on the group I'm looking to pan to and use that with the panZoomInstance.getPan() and getSizes() information, but my experiments are not working out.
I'd like to accomplish the same king of animation as their example (http://ariutta.github.io/svg-pan-zoom/demo/simple-animation.html) but center the viewport to the item.
Against all odds I was able to figure out this part of it!
function customPanByZoomAtEnd(amount, endZoomLevel, animationTime){ // {x: 1, y: 2}
if(typeof animationTime == "undefined"){
animationTime = 300; // ms
}
var animationStepTime = 15 // one frame per 30 ms
, animationSteps = animationTime / animationStepTime
, animationStep = 0
, intervalID = null
, stepX = amount.x / animationSteps
, stepY = amount.y / animationSteps;
intervalID = setInterval(function(){
if (animationStep++ < animationSteps) {
panZoomInstance.panBy({x: stepX, y: stepY})
} else {
// Cancel interval
if(typeof endZoomLevel != "undefined"){
var viewPort = $(".svg-pan-zoom_viewport")[0];
viewPort.style.transition = "all " + animationTime / 1000 + "s ease";
panZoomInstance.zoom(endZoomLevel);
setTimeout(function(){
viewPort.style.transition = "none";
$("svg")[0].style.pointerEvents = "all"; // re-enable the pointer events after auto-panning/zooming.
panZoomInstance.enablePan();
panZoomInstance.enableZoom();
panZoomInstance.enableControlIcons();
panZoomInstance.enableDblClickZoom();
panZoomInstance.enableMouseWheelZoom();
}, animationTime + 50);
}
clearInterval(intervalID)
}
}, animationStepTime)
}
function panToElem(targetElem) {
var initialSizes = panZoomInstance.getSizes();
var initialLoc = panZoomInstance.getPan();
var initialBounds = targetElem.getBoundingClientRect();
var initialZoom = panZoomInstance.getZoom();
var initialCX = initialBounds.x + (initialBounds.width / 2);
var initialCY = initialBounds.y + (initialBounds.height / 2);
var dX = (initialSizes.width / 2) - initialCX;
var dY = (initialSizes.height / 2) - initialCY;
customPanByZoomAtEnd({x: dX, y: dY}, 2, 700);
}
The key was in calculating the difference between the center of the viewport width & height from panZoomInstance.getSizes() and the center of the target element's client bounding rectangle.
Now the issue is trying to make an animated zoom. For now I've made it do a zoom to a specified location with a command at the end of the panning animation and set some css to make the zoom a smooth transition. The css gets removed after some time interval so normal zooming and panning isn't affected. In my attempts to make the zoom a step animation it always appeared to zoom past the intended max point.
I've built a Force Simulation in D3.js 4.4 that uses the built in zoom + pan functions (d3.zoom()), combined with a selection-rectangle for selecting nodes (like here or here).
Dragging/panning the viewport around is the default behaviour, but is filtered out when the Shift-key is held. Then another drag-handler takes over and draws a rectangle from the starting point. On mouseup the code intersects the rectangle with the nodes on screen and selects them.
This all works fine, but the whole thing goes sideways when I apply zoom behavior to the equation. I've pinpointed the problem to the scale values d3.event.transform applies to the g-container that contains the nodes.
Due to the nature of this project (and the internet-less dev environment we're working in) I can't copy/paste the working code on the web. So here are the abridged parts that are causing the problem (I had to re-type these from another monitor).
var currentZoom = null;
var zoom = d3.zoom()
.scaleExtent([0.5, 10])
.filter(function() {
return !d3.event.shiftKey // Don't zoom/pan when shift is pressed
})
.on("zoom", zoomed);
function zoomed() {
currentZoom = d3.event.transform;
d3.select("g.links").attr("transform", d3.event.transform);
d3.select("g.nodes").attr("transform", d3.event.transform);
}
// This fires on mouseup after dragging the rectangle (svg rect)
function intersectSelectionRect() {
var rect = svg.select("rect.selection-rect");
// Intersect rectangle with nodes
var lx = parseInt(rect.attr("x"));
var ly = parseInt(rect.attr("y"));
var lw = parseInt(rect.attr("width"));
var lh = parseInt(rect.attr("height"));
// Account for zoom
if (!!currentZoom) {
// Recalculate for pan
lx -= currentZoom.x;
ly -= currentZoom.y;
// Recalculate for zoom (scale)
// currentZoom.k ???????????????
}
// Rectangle corners
var upperRight = [lx + lw, ly];
var lowerLeft = [lx + ly + lh];
nodes.forEach(function(item, index) {
if ((item.x < upperRight[0] && item.x > lowerLeft[0]) &&
(item.y > upperRight[1] && item.y < lowerLeft[1])) {
item.selected = true;
}
});
}
As you can (hopefully) make out, the problem occurs when recalculating the rect x and y with the zoom k (scale). If the scale is 1 (starting value) the recalculation is correct. If it's anything other than that the actual selection rectangle no longer matches the one drawn on screen.
If everything is setup properly you should be able to apply the transform to the points,
var upperRight = [lx, ly];
var lowerLeft = [lx + lw, ly + lh];
if (!!currentZoom) {
upperRight = currentZoom.apply(upperRight);
lowerLeft = currentZoom.apply(lowerLeft);
}
I have a d3 graph that allows for pan and zoom behavior. It also has subGraph functionality that allows a user to expand and collapse the subGraph. When the user clicks on the icon to open and close the subGraph this code runs:
btn.on("click", function(d:any, i:any) {
parentNode = nodeObject; // gives me he x and y coordinates of the parent node
d3.event["stopPropagation"]();
e["subGraph"].expand = !expand; // add or remove subGraph
window.resetGraph = !window.resetGraph;
console.log(window.resetGraph);
if (window.resetGraph) {
window.scale = window.zoom.scale();
window.translate = window.zoom.translate();
window.zoom.scale(1).translate([0 , 0]);
} else {
window.zoom.scale(window.scale).translate(window.translate);
}
console.log(window.zoom.scale());
console.log(translate);
renderGraph();
});
I'm essentially adding and removing the subGraph property of the node on click of the icon. Then redrawing the graph completely.
I can get the x and y coordinates of the parent container, but if I am re-rendering the graph, how can I go about rendering the graph so that the graph stays at the same scale and translation as it was when I clicked on the toggle subGraph icon. I am trying to redraw the graph at the same position as it was before just with the subGraph either expanded or collapsed. Below is the code that seems to be fighting me when the graph renders:
var scaleGraph = function() {
var graphWidth = g.graph().width + 4;
var graphHeight = g.graph().height + 4;
var width = parseInt(svg.style("width").replace(/px/, ""), 10);
var height = parseInt(svg.style("height").replace(/px/, ""), 10);
var zoomScale = originalZoomScale;
// Zoom and scale to fit
if (ctrl.autoResizeGraph === "original") {
zoomScale = initialZoomScale;
}
translate = [(width / 2) - ((graphWidth * zoomScale) / 2) + 2, 1];
zoom.center([width / 2, height / 2]);
zoom.size([width, height]);
zoom.translate(translate);
zoom.scale(zoomScale);
zoom.event(svg);
};
If I check if the toggle Icon has been clicked, can I redraw the graph with the same scale and translation as before it was redrawn?
Thanks
I just solved it by keeping track of the current scale and translation values:
zoom.on("zoom", function() {
svgGroup.attr("transform", "translate(" + (<any>d3.event).translate + ")" + "scale(" + (<any>d3.event).scale + ")");
// keep track of the currentZoomScale and currentPosition at all times
currentZoomScale = (<any>d3.event).scale;
currentPosition = (<any>d3.event).translate;
});
Then when re-rendering the graph in my scaleGraph function above I just checked to see if the graph was re-rendered because of the subGraph being toggled, if it has then I use the current scale and translation values:
if (ctrl.autoResizeGraph !== "original" && togglingSubGraph) {
zoomScale = currentZoomScale; // render the graph at its current scale
translate = currentPosition; // render the graph at its current position
}
I have created one fiddle for you.
fiddle
Please check the function refreshGraph
var refreshGraph =function(){
window.resetGraph = !window.resetGraph;
console.log(window.resetGraph);
if(window.resetGraph){
window.scale = window.zoom.scale();
window.translate = window.zoom.translate();
window.zoom.scale(1).translate([0,0]);
}else{
window.zoom.scale(window.scale)
.translate(window.translate);
}
console.log(window.zoom.scale());
console.log(translate);
draw();
}
I have used window.zoom (used window to let you know that this is shared)
Button reset toggles the zoom level to 0 scale and toggles back to where it was if clicked again.
Hope it helps.
TL;DR prone?
In order to fire a click event of column, one must click in the column. That's hard to do when the column has little or no discernible height. How can I make the area above/below a column (in an unstacked column chart) change mouse cursor to pointer and subsequent mouse click fire the point's click event?
Care for more?
I have an unstacked column chart where there is a large discrepancy in value. This results in some columns being very small, basically invisible to the user.
This proves problematic when trying to fire a click event on the small columns as from what I can tell the only way to get a click to fire the point click event is if it's within the column itself. I'm wiring the click event as
plotOptions: {
series: {
cursor: 'pointer',
point: {
events: {
click: function () {
alert('Category: ' + this.category + ', value: ' + this.y);
}
}
}
}
}
The following JSFiddle is an example of this problem (The Dec point).
I understand this limitation if columns were stacked and also know a workaround is to specify a minimum point length. However I expect, when columns are not stacked, there is a way to make clicking on any area along the vertical of chart within the x axis boundaries of the column would fire the click event.
The yellow area in image below is what I'm trying to make clickable to fire the point event for the 'Dec' column.
Mousing over this area should change the pointer to cursor and when mouse clicked, the click event for corresponding column should be triggered.
JSFiddle
Approach: Hook up mousemove and click events on the chart container.
In the mousemove event, get matching point based on event offset coordinates and combo of chart + point plotting.
Change mouse cursor to cursor and state to hover (when appropriate).
$('#container').on('mousemove', function(event) {
var chart = $('#container').highcharts();
if (chart.series.length === 1) {
$('#container').css('cursor', 'default');
for (var seriesIdx = 0; seriesIdx < chart.series.length; seriesIdx++) {
var series = chart.series[seriesIdx];
for (var pointIdx = 0; pointIdx < series.data.length; pointIdx++) {
var point = series.data[pointIdx];
var height = chart.plotTop + point.plotY;
var min = chart.plotLeft + point.plotX - (point.pointWidth / 2);
var max = chart.plotLeft + point.plotX + (point.pointWidth / 2);
var isWithin = event.offsetX >= min && event.offsetX <= max;
if (isWithin && event.offsetY < height) {
$('#container').css('cursor', 'pointer');
point.setState('hover');
point.hasExtendedHover = true;
} else {
if (point.hasExtendedHover) {
point.setState();
point.hasExtendedHover = false;
}
}
}
}
}
});
Similar for click event but break out on point found because thee is no cursor and point state cleanup needed on other points.
$('#container').on('click', function(event) {
var chart = $('#container').highcharts();
var found = false;
for (var seriesIdx = 0; seriesIdx < chart.series.length; seriesIdx++) {
var series = chart.series[seriesIdx];
for (var pointIdx = 0; pointIdx < series.data.length; pointIdx++) {
var point = series.data[pointIdx];
var min = chart.plotLeft + point.plotX - (point.pointWidth / 2);
var max = chart.plotLeft + point.plotX + (point.pointWidth / 2);
if (event.offsetX > min && event.offsetX < max) {
point.firePointEvent('click');
found = true;
break;
}
}
if (found) {
break;
}
}
});
Issues:
I can't seem to get this to work for multiple series using
categories. The plotX and plotY values of points appear to mean something
different than when there is one series.
The mousemove event on the container does not
always fire, especially on multiple series. My hypothesis is there is
something stopping propagation of this event within the chart so
it's not bubbling up to the container. It's sporadic so hard to know
for sure what's happening.
If you mouse over above a column then go into the column the hover state goes away, annoying little tick that still itches after 20+ ways of scratching. IE10+ does not have this issue (wha?! +1 for IE)
Following on from this question here:
Calculate distance to move a box to remove intersection
I have a problem where moving one box causes it to overlap with another box, and the algorithm as it currently stands will just move the first box back in (almost) the direction it came from causing it to overlap the first box again and so forth. Is there a good way to avoid this situation? I feel like remembering the vector from last time and somehow combining it with the new proposed move might be the solution.
Here's a simple fiddle:
http://jsfiddle.net/x8MT3/4/
Notice how the one box is moved, but then overlaps the other box, so the box is moved again back in the opposite direction and after 6 cycles, is still overlapping.
Here's the code for the add method - it adds boxes to a list, checking the current members for otherlaps:
self.add = function (item, iteration) {
// check intersections with existing boxes
iteration = iteration || 0;
if (iteration < 6) {
for (var i = 0; i < boxes.length; i++) {
var stationary = boxes[i];
var boundsA = getBounds(item);
var boundsB = getBounds(stationary);
if (doesIntersect(boundsA, boundsB)) {
item.elem.addClass("overlapped");
// move item
// 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: stationary.x + stationary.width / 2,
y: stationary.y + stationary.height / 2
};
var line = {
x1: centerA.x,
y1: centerA.y,
x2: centerB.x,
y2: centerB.y
}
var vector = {
x: Math.min(item.x + item.width, stationary.x + stationary.width) - Math.max(item.x, stationary.x),
y: Math.min(item.y + item.height, stationary.y + stationary.height) - Math.max(item.y, stationary.y)
};
var signX = line.x1 - line.x2 > 0 ? 1 : -1;
var signY = line.y1 - line.y2 > 0 ? 1 : -1;
item.x = item.x + vector.x * signX;
item.y = item.y + vector.y * signY;
item.elem.offset({
left: item.x,
top: item.y
});
return self.add(item, iteration + 1);
}
}
}
boxes.push(item);
}
Why do not you use iterative process ... ???
as I see it you want to rearrange boxes so they not overlap
but do not change the layout too much
if you do not mind some space between boxes (the will not touch) then:
Algorithm:
1.find overlapped box
red box
2.find movement direction
green vector
between box centers
then change it size to movement step (few pixels...)
3.compute new position of overlapped
then check for new overlaps
if too close to another box stop movement in that direction
Blue arrows shows collision stop
4.apply this (loop from bullet 1) for all overlapped boxes
do just one or few steps
not the whole movement !!!
5.iteratively loop all (from bullet 1)
stop when no overlapped box found
or process has timeout-ed ...
because this can get also stuck
[Notes]
if box centers are the same
then add random direction to movement vector
the whole process can be applied more times with decreasing step for more speed