Highcharts: locating column group information on mouseover - javascript

I'm working on a rather complex Highcharts implementation and have run into a snag. I'm using a grouped column chart, with a series data structure very much like this:
http://jsfiddle.net/gh/get/jquery/1.7.2/highslide-software/highcharts.com/tree/master/samples/highcharts/demo/column-basic/
e.g.
series: [{
name: 'Tokyo',
data: [49.9, 71.5, 106.4, 129.2, 144.0, 176.0]
}, {
name: 'New York',
data: [83.6, 78.8, 98.5, 93.4, 106.0, 84.5]
},
...
When the user mouses over any of the columns in a particular month, I'm trying to capture the following:
the index of the month group (e.g. Jan == 0, Feb == 1 or something like that)
the x,y and height,width coordinates of the entire group.
The problem is that "this" in plotOptions.column.events.mouseOver contains such a huge volume of data, I've been unable to identify where in the massive data structure that info exists. Also, it appears the mouseOver event is tied to the individual column being moused-over, rather than the group itself.
Any idea? (Thanks in advance!)
Ben

So I spent a bit of time looking into this and you're right, the event object passed into the mouseOver method is not all that helpful. I tried finding a reference to the currently-highlighted column, but to no avail. But, I found an alternate approach, relying on the DOM structure for the chart. This means it would probably break if they ever change the chart layout in a future version, but it should work for now.
The basic structure of the chart looks like this:
<g class="highcharts-series-group">
<g class="highcharts-series">
<rect x="11" y="223" width="5" height="55" />
<rect x="61" y="199" width="5" height="79" />
<rect x="111" y="160" width="5" height="118" />
<rect x="161" y="135" width="5" height="143" />
<rect x="211" y="118" width="5" height="160" />
<rect x="261" y="83" width="5" height="195" />
<!-- More <rect/>'s -->
</g>
<g class="highcharts-series">
<rect x="19" y="185" width="5" height="93" />
<rect x="69" y="191" width="5" height="87" />
<rect x="119" y="169" width="5" height="109" />
<rect x="169" y="175" width="5" height="103" />
<rect x="219" y="161" width="5" height="117" />
<rect x="269" y="184" width="5" height="94" />
<!-- More <rect/>'s -->
</g>
<!-- More <g class="highcharts-series"/>'s -->
</g>
These correspond directly to the series information that was passed in on chart creation, with the x, y, width, and height attributes that I believe you're referring to. We can use this structure to use some old-fashioned event handling to retrieve the information you're looking for.
// Loop over each series element and bind a
// 'mouseover' event to it.
$('.highcharts-series rect').on('mouseover', function() {
var self = $(this);
// Determine the offset (equal to the number of 'rect'
// objects before it)
var xIndex = self.prevAll('rect').length;
var groupInfo = [];
// Retrieve the '.highcharts-series-group' node
// NOTE: $(this).parents('.highcharts-series-group')
// does not appear to work (perhaps a limitation with
// jQuery and SVG?)
$(this.parentNode.parentNode)
// Loop over all series nodes within the current chart
.find('.highcharts-series')
.each(function(){
// Retrieve the matching entry within each series
// (represented by the nth 'rect' node)
var element = $(this).find('rect:eq(' + xIndex + ')');
if (!element.length) {
return;
}
// Populate the Group Info element
groupInfo.push({
x: element.attr('x'),
y: element.attr('y'),
width: element.attr('width'),
height: element.attr('height')
});
});
// Do what you want with the groupInfo object and xIndex here:
console.log(xIndex, groupInfo);
});

In mouseover venet for point, you have access to all parameters about column.
http://jsfiddle.net/amKuZ/
mouseOver: function() {
$report.html('coordinate: x: ' +this.plotX+'coordinate y: ' +this.plotY+'column width' + this.pointWidth + ' x value:'+this.x);
}
http://api.highcharts.com/highcharts#plotOptions.column.point.events.mouseOver

Related

How to get the exact BBox for svg <tspan>

I am trying to figure out why getBBox() for tspan element of a svg does not return the dimension.
To demonstrate this with an example, if I run BBox on both tsp1 and rect1, it returns the correct dimension for rect1 but not for tsp1
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log(tspBBox);
console.log(rectBBox);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>
I was expecting BBox to return the exact x and y for tsp1 but it does not.
I don't know why. I need to pass on the exact values to the succeeding class dynamically.
How can javascript return the exact dimension for the tspan element?
There are a number of methods for measuring text, and they are a bit more complex than defining a simple box. This is because with the dx, dy and rotate attributes, each addressable character can be be positioned individually - moved and rotated in every direction. Therefore, it makes more sense to answer the question where a single character is positioned, and where, after completing one sequence, the next character would be positioned.
In your case none of the above attributes are set ( on the <tspan> or <text> element). In this case is is possible to retrieve the start position of the <tspan> with .getStartPositionOfChar(0) and the horizontal width with .getComputedTextLength().* The height according to the font metrics is the same for all characters in the tspan, so it is enough to return one .getExtentOfChar(0) - 0 refers to the first character within the sequence of addressable characters.
As chrwahl pointed out in his answer, the start position refers to the font-specific baseline and normally will not be identical to the top left corner of a bounding box.
*There is a subtle trick here: if the letter-spacing or word-spacing CSS properties were defined, the "length" returned would not only return the width from the start of the first character to the end of the last, but also would add (or subtract) a spacing value that is defined after the end of the string. In other words: despite its name, the method returns the relative horizontal start position of the next character after the string examined.
var tsp = document.getElementById('tsp1');
var tspPos = tsp.getStartPositionOfChar(0);
console.log('start position', tspPos.x, tspPos.y);
console.log('horizontal advance', tsp.getComputedTextLength());
console.log('vertical extent', tsp.getExtentOfChar(0).height);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
</svg>
It is all about the dominant-baseline. So, there is a differences between where the text is placed according to the dominant-baseline and the box that the text takes up. The value text-before-edge will place the text according to the upper left corner of the box.
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log('tspBBox', tspBBox.x, tspBBox.y);
console.log('rectBBox', rectBBox.x, rectBBox.y);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 100 400 200">
<text class="t1" id="t1" font-size="20" font-family="PT Mono"
text-decoration="underline" dominant-baseline="text-before-edge">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>

The XY coordinates in a d3 drag event are inverting

I have a force layout with the following structure:
<svg width="300" height="220">
<g class="scaleWrapper" transform="scale(0.3)">
<g class="transformWrapper" transform="translate(-110, -80)">
<g class="backgroundWrapper">
<rect class="backgroundRect" width="300" height="220"></rect>
</g>
<g class="forceNode"></g>
<g class="forceNode"></g>
<g class="forceLink"></g>
</g>
</g>
</svg>
I also have a drag behavior linked to the rect to drag it around (scaling is handled through a separate slider).
let transformElement = d3.select('.transformWrapper');
let svgBackground = transformElement.append('g')
.classed('backgroundWrapper', true);
function originFunction() {
let d = d3.select('.transformWrapper');
return {
x: d.attr('x'),
y: d.attr('y')
};
}
let svgDrag = d3.behavior.drag()
.origin(originFunction)
.on('dragstart', function(){
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function(){
transformElement.attr("transform", `translate(${d3.event.x}, ${d3.event.y})`);
});
svgBackground.call(svgDrag);
It mostly works, but it jumps around as i drag it. I did a log and saw that the d3.event object is alternating between relative XY coordinates and absolute ones, here's a sample of what I'm seeing:
-111 -80
-29 -6
-110 -80
-29 -5
I don't see any other elements that have behavior bound to them. All the d3.event objects have the 'drag' type property and the same srcElement. How can I silence the events that are returning the relative positions?
Found the problem, it's the same bug as in d3.event.y has strange values during drag behavior
Moving the drag behavior to the transform group fixed it right up.

Checking text overflow on SVG textpath & jquery

First of all I looked at all possible related answers here but none of them seem to bring the answer I need so here I am.
Given a svg text path:
<svg viewBox="0 0 900 900"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="mysvg"
>
<defs>
<path id="myPath" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
</defs>
<use xlink:href="#myPath" fill="none" stroke="red" />
<text id="names" font-family="Verdana" font-size="10" text-anchor="middle" >
<textPath xlink:href="#myPath" startOffset="50%">
My text is going to exceed at some point…
</textPath>
</text>
</svg>
At this point the text exceeds the textpath
I can't find a way to check for possible overflow through jquery. This command won't actually return undefined:
alert($("text#names").attr("textLength") );
I am trying to check for overflows in order to fit the text to the maximum length or so.
I had the same problem when adjusting font size so that the given text will be drawn with the largest possible font size without overflow. Its quite simple using plain JS.
1) Determine the np. of characters in the text element with a minimum font size:
textElement.css('font-size', 1);
var allCharCount = textElement[0].getNumberOfChars();
2) Then set font size to any value and determine the length again
var hasOverflow = allCharCount != textElement[0].getNumberOfChars();
getNumberOfChars() will only return the no. of chars drawn. If there is an overflow this number will be smaller then from the original whole string.
It looks like text.getNumberOfChars() has changed since the other answer was written, and now returns the total number of characters in the string, regardless of if they're rendered or not.
My approach to this problem is to:
Change the <textPath> element to draw on a much longer path, then calculate the text length using text.getComputedLength()
Change the <textPath> back to the original path and calculate length again
If the length on the original path is shorter than the length on the longer path, you know there's an overflow.
const textPath = document.querySelector('textPath');
const checkClipped = () => {
textPath.setAttribute('xlink:href', '#fullWidthPath');
const fullLength = textPath.getComputedTextLength();
textPath.setAttribute('xlink:href', '#myPath');
const curvedLength = textPath.getComputedTextLength();
return fullLength > curvedLength;
}
const findLongestString = () => {
const text = textPath.innerHTML;
if (checkClipped()) {
const newText = text.substring(0, text.length - 1);
textPath.innerHTML = newText;
return findLongestString(newText);
} else {
return text;
}
}
console.log(findLongestString())
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="mysvg">
<defs>
<path id="myPath" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
<path id="fullWidthPath" d="M 0 0 L 0 10000" />
</defs>
<use xlink:href="#myPath" fill="none" stroke="red" />
<text id="names" font-family="Verdana" font-size="10" text-anchor="middle" >
<textPath xlink:href="#myPath" startOffset="50%">
My text is going to exceed at some point…
</textPath>
</text>
</svg>

Setting D3 svg.transition to go from slow to fast to slow

I have a D3 graph that allows a user to click a button to take them to a specified node. The button looks like this:
<button type="button" class="btn btn-primary" ng-click="ctrl.panGraph(9)">Go to End</button>
This button will take the user from wherever they are in the svg at the time of click, to the x and y coordinates of the last node, with the id of 9. On click this function is called:
function panGraph (nodeId:any) {
svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);
for (var i = 0; i < renderedNodes.length; i++) {
if (nodeID === renderedNodes[i].id) {
ctrl.selectedNode = renderedNodes[i];
var translate = [svgWidth / 2 - renderedNodes[i].x, svgHeight / 2 - renderedNodes[i].y];
var scale = 1;
svg.transition().duration(4000).ease(d3.easeExpInOut).call(zoom.translate(translate).scale(scale).event);
}
}
}
In the above function I have all the rendered nodes that have been rendered on the page, once I find the matching id I use its x and y coordinates to center the specified node in the middle of the svg. That all works fine.
I am trying to use some animations during the time that the graph is translating to the specified node on button click. When the user clicks the button that takes him or her to the specified node, is it possible to animate the transition so that the transition initially starts slow, then speeds up, but then slows down again at the end as it gets close to the specified node? Thanks
UPDATE:
The above code with the "ease" incluided gives me this console error:
angular.js:13550 TypeError: Cannot read property 'indexOf' of undefined
at Object.d3.ease (d3.js:5844)
at Array.d3_transitionPrototype.ease (d3.js:8838)
at zoomOnNode (DiagramComponent.ts:1128)
at DiagramComponent.ts:1072
at Scope.$digest (angular.js:17073)
at Scope.$apply (angular.js:17337)
at HTMLButtonElement.<anonymous> (angular.js:25023)
at HTMLButtonElement.dispatch (jquery.js:4737)
at HTMLButtonElement.elemData.handle (jquery.js:4549)
Here is the v3 equivalent to Gerardo's post regarding v4:
svg.transition().duration(1000).ease("exp-in-out").call(zoom.translate(translate).scale(scale).event);
For a list of all the easing equivalents from v3 to v4 and other changes:
https://github.com/d3/d3/blob/master/CHANGES.md
One (out of several) solution is to use ease with d3.easeExpInOut, or d3.easePolyInOut.exponent(x) with a high exponent (like x=4 or x=5).
See this snippet. Click the circle to see it moving from left to right, starting slow, speeding up and then slowing down again:
d3.select("circle").on("click", function(){
d3.select(this).transition()
.duration(4000)
.ease(d3.easeExpInOut)
.attr("cx", 360)
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="200">
<circle cx="40" cy="100" r="30" fill="teal"></circle>
<line x1="40" x2="40" y1="100" y2="150" stroke="black" stroke-width="1"></line>
<line x1="360" x2="360" y1="100" y2="150" stroke="black" stroke-width="1"></line>
</svg>

How to add a second tooltip to highcharts

I would like to add an external popover to highcharts. I'm currently looking at using WebUI Popover
So the standard highcharts popover displays the standard data, however, I would like to use WebUI Popover to display some data to explain a specific column, which can be fetched from the DB.
The actual fetching of data etc is fine, but I can't figure out how to display a specific popover for a column
The way WebUI Popover works is that it requires some HTML identifier to know where to show the popover:
$(#identifier).webuiPopover({"content"})
I cant find any identifier to use to link the popover to each column:
<g class="highcharts-series highcharts-tracker" visibility="visible" zIndex="0.1" transform="translate(55,48) scale(1 1)" style="" clip-path="url(#highcharts-3)">
<rect x="63.5" y="33.5" width="119" height="162" stroke="#FFFFFF" fill="rgb(124, 181, 236)" rx="0" ry="0" stroke-width="1" data-target="webuiPopover0"></rect>
<rect x="309.5" y="98.5" width="119" height="97" stroke="#FFFFFF" fill="rgb(124, 181, 236)" rx="0" ry="0" stroke-width="1"></rect>
<rect x="555.5" y="65.5" width="119" height="130" stroke="#FFFFFF" fill="rgb(124, 181, 236)" rx="0" ry="0" stroke-width="1"></rect>
</g>
So I want to show a popover for each coloumn (rect), but I really don't know how. I've tried:
var thePoint = "rect[x='" + 63.5 + "'][y='" + 33.5 + "']";
$(thePoint).webuiPopover(...)
This works to some extent, but obviously I've hard coded the 63,5 and 33,5. I've tried everything to dynamically get the x and y values, but I can never get those exact numbers.
Any other suggestions? Maybe if someone could explain how the built in popover gets the position?
There is this demo of clickable points: http://www.highcharts.com/demo/line-ajax
It uses Highslide.
If you want to use WebUI Popover, then you can find columns' rectangles and add popups. Example: http://jsfiddle.net/j57me5w1/
Looks like popups will start from top left corner.
$(function () {
$('#container').highcharts({
series: [{
type: 'column',
data: [1, 2, 3, 4, 5, 6, 7]
}]
});
var columns = $('.highcharts-tracker > rect');
$.each(columns, function (i, c) {
$(c).webuiPopover({
title: 'test',
content: '<div id="popup">popup content</div>',
closeable: true,
arrow: true,
placement: 'auto'
});
});
});

Categories