How can I show dataLabels or tooltip for outer circle of packedbubble?
For example in Carbon emissions around the world (2014) sample, i need to show a tooltip for each continent, when hovering on them. or if possible show dataLabels for each of them
Notice that the outer circle is just a path without any properties like x, y etc, so it wouldn't work with the Highcharts tooltip feature. The only solution which came to my mind is to create a custom tooltip on mouseover event on this path.
Demo: https://jsfiddle.net/BlackLabel/9gkdfsnj/
Code:
events: {
render() {
var
chart = this,
series = chart.series[0],
graphic = series.parentNode.graphic,
tooltip = document.getElementById('tooltip'),
text;
text = "Sum value: " + Math.floor(series.yData.reduce((a, b) => a + b, 0));
graphic.element.onmouseover = function() {
tooltip.style.visibility = "visible";
tooltip.innerHTML = text;
tooltip.style.left = graphic.x + (tooltip.offsetWidth /2) - chart.plotLeft + 'px';
tooltip.style.top = graphic.y + graphic.height / 2 + 'px'
}
graphic.element.onmouseout = function() {
tooltip.style.visibility = "hidden"
}
}
}
It is just a simple example, feel free to improve it.
Add value key
[{
name: 'Animals',
value: 867,
data: [{
name: 'Lion',
value: 167
}, {
name: 'Croatia',
value: 200
},
{
name: "Dog",
value: 97
}],
}]
Add below lines inside Tooltip
tooltip: {
useHTML: true,
formatter : function(){
let childtooltip = "";
if(this.point != undefined && this.point != null){
if((this.point.name != undefined && this.point.name != null) && (this.point.value != undefined && this.point.value != null)){
childtooltip = `<b>${this.point.name}:</b> ${this.point.value}`;
}
}
if(childtooltip == ""){
return `<b>${this.series.userOptions.name}:</b> ${this.series.userOptions.value}<br>`
}else{
return `${childtooltip}`;
}
}
}
Demo
Related
I have a highcharts sankey diagram with two sides:
There are situations where some of my nodes have empty links (=with 0 weight). I would like the node to being displayed despite having no link from or to it.
Any chance I can achieve this?
I read on this thread that I have to fake it with weight=1 connexions, I could make the link transparent, and twitch the tooltip to hide those, but that's very painful for something that feels pretty basic.
Maybe a custom call of the generateNode call or something?
Thanks for the help
You can use the following wrap to show a node when the weight is 0.
const isObject = Highcharts.isObject,
merge = Highcharts.merge
function getDLOptions(
params
) {
const optionsPoint = (
isObject(params.optionsPoint) ?
params.optionsPoint.dataLabels : {}
),
optionsLevel = (
isObject(params.level) ?
params.level.dataLabels : {}
),
options = merge({
style: {}
}, optionsLevel, optionsPoint);
return options;
}
Highcharts.wrap(
Highcharts.seriesTypes.sankey.prototype,
'translateNode',
function(proceed, node, column) {
var translationFactor = this.translationFactor,
series = this,
chart = this.chart,
options = this.options,
sum = node.getSum(),
nodeHeight = Math.max(Math.round(sum * translationFactor),
this.options.minLinkWidth),
nodeWidth = Math.round(this.nodeWidth),
crisp = Math.round(options.borderWidth) % 2 / 2,
nodeOffset = column.sankeyColumn.offset(node,
translationFactor),
fromNodeTop = Math.floor(Highcharts.pick(nodeOffset.absoluteTop, (column.sankeyColumn.top(translationFactor) +
nodeOffset.relativeTop))) + crisp,
left = Math.floor(this.colDistance * node.column +
options.borderWidth / 2) + Highcharts.relativeLength(node.options.offsetHorizontal || 0,
nodeWidth) +
crisp,
nodeLeft = chart.inverted ?
chart.plotSizeX - left :
left;
node.sum = sum;
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
if (1) {
// Draw the node
node.shapeType = 'rect';
node.nodeX = nodeLeft;
node.nodeY = fromNodeTop;
let x = nodeLeft,
y = fromNodeTop,
width = node.options.width || options.width || nodeWidth,
height = node.options.height || options.height || nodeHeight;
if (chart.inverted) {
x = nodeLeft - nodeWidth;
y = chart.plotSizeY - fromNodeTop - nodeHeight;
width = node.options.height || options.height || nodeWidth;
height = node.options.width || options.width || nodeHeight;
}
// Calculate data label options for the point
node.dlOptions = getDLOptions({
level: (this.mapOptionsToLevel)[node.level],
optionsPoint: node.options
});
// Pass test in drawPoints
node.plotX = 1;
node.plotY = 1;
// Set the anchor position for tooltips
node.tooltipPos = chart.inverted ? [
(chart.plotSizeY) - y - height / 2,
(chart.plotSizeX) - x - width / 2
] : [
x + width / 2,
y + height / 2
];
node.shapeArgs = {
x,
y,
width,
height,
display: node.hasShape() ? '' : 'none'
};
} else {
node.dlOptions = {
enabled: false
};
}
}
);
Demo:
http://jsfiddle.net/BlackLabel/uh6fp89j/
In the above solution, another node arrangement would be difficult to achieve and may require a lot of modifications beyond our scope of support.
You can consider using mentioned "tricky solution", since might return a better positioning result. This solution is based on changing 0 weight nodes on the chart.load() event and converting the tooltip as well, so it may require adjustment to your project.
chart: {
events: {
load() {
this.series[0].points.forEach(point => {
if (point.weight === 0) {
point.update({
weight: 0.1,
color: 'transparent'
})
}
})
}
}
},
tooltip: {
nodeFormatter: function() {
return `${this.name}: <b>${Math.floor(this.sum)}</b><br/>`
},
pointFormatter: function() {
return `${this.fromNode.name} → ${this.toNode.name}: <b>${Math.floor(this.weight)}</b><br/>`
}
},
Demo:
http://jsfiddle.net/BlackLabel/0dqpabku/
Seems like Highcharts is skipping a few data points in the shared tooltip for high number of data points (2500+).
I am trying to render a dual axis chart with 2500+ data points for 4 series - using Highcharts. I am also using a shared tooltip option to render my custom tooltip html. But at times Highcharts skips 1 or 2 data points in the tooltip. For example, when I slowly hover over each of the points from left to right, then I am supposed to see '1st April' after '31st March'. But instead, I see '2nd April'. Is it a bug? Or am I missing something? (I have verified that all the dates are present in the categories passed to the Highcharts.)
tooltip: {
borderColor: '#ccc',
backgroundColor: 'transparent',
borderWidth: 0,
shadow: false,
shared: true, //show all series values together
useHTML: true,
// hideDelay: 50000,
formatter: function() {
if (props.config.type == 'pie') {
return 'Value : ' + this.y;
} else {
let html = '<div class="fixed-tooltip">';
html += formatTooltipDate(this.x);
if (this.points &&
this.points.length > 1 &&
props.config.type != "combination") { //multiple series*(see note below)
//*combination series are having 1 point, so handled in the else section as single series.
let dateIndex = props.config.data.categories.indexOf(this.x);
console.log(" date ", this.x);
console.log(" dateIndex ", dateIndex);
if (props.config.type == "dual") {
let dualAxisTitles = props.config.dualAxisTitles;
html += formatDualSeriesTooltipData(this.x, dateIndex, this.points, dualAxisTitles);
} else {
html += formatMultiSeriesTooltipData(this.x, dateIndex, this.points);
}
} else { //single series
//for combination charts have a custom tooltip logic
if (props.config.type == "combination") {
let dateIndex = props.config.data.categories.indexOf(this.x);
html += formatMultiSeriesTooltipData(this.x, dateIndex, props.config.data.series);
} else {
let seriesColor = this.points[0].point.series.color;
let seriesName = this.points[0].point.series.name;
let value = this.points[0].y;
html += formatSingleSeriesTooltipData(value);
}
}
html += '</div>';
return html;
}
}
}
Expected to see a tooltip for "1st April" data point, after "31st March". Instead seeing tooltip for "2nd April" data point.
The points are skipped if there is no enough space for them in the plot area (1px for 1 point). The solution is to set a adequate chart width:
chart: {
width: 1000
},
Live demo: http://jsfiddle.net/BlackLabel/yjk0ta43/
API: https://api.highcharts.com/highstock/chart.width
I have a highcharts scatter plot for which I'm trying to fetch the x and y tickPositions in freemarker template, specifically the first and last one. Such as that I would get something as [-10,-10] (bottom left corner) at the intersection of the x-y axis and [30,40] (top right corner) at the intersection of the opposite sides, where xAxis ticks are [-10,0,10,20,30] and yAxis ticks are [-10,0,10,20,30,40]
I want these points so that I'll be able to plot a diagonal line across the scatter plot from bottom lower corner to top right corner. The line series should look like:
series: [
{
type : 'line',
<#--diagonal line-->
data :[[-10,-10], [30,40]], // to be calculated dynamically
lineWidth: 0.5,
marker : {
enabled : false
}
},
{
color: 'rgb(0,85,152)',
data: [[2,3],[6,7],[8,9]]
}
]
The problem at present is I'm unable to get [-10,-10], [30,40] data points. Is it even possible is what I'm wondering. Any help is much appreciated!
You have the getExtremes() function on an axis.
For example:
var extremes = $('#container').highcharts().yAxis[0].getExtremes();
Here is the doc, and here is a demo fiddle
Is this what you were trying to achieve ?
Edit
After your fiddle example, I understand better your need.
Here is the updated fiddle
var chart = $('#container').highcharts();
var extremeY = chart.yAxis[0].getExtremes();
var extremeX = chart.xAxis[0].getExtremes();
var lineSeries = {
type: 'line',
data: [
[extremeX.min, extremeY.min],
[extremeX.max, extremeY.max]
],
lineWidth: 0.5,
lineColor: 'rgb(0,0,0)',
marker: {
enabled: false
}
};
chart.addSeries(lineSeries);
I created an object with the properties of the line series. And using min and max (not dataMin and dataMax) properties from the object returned by getExtremes() you obtain the desired result.
Edit 2
You could put this code in the load event of the chart. It is a callback called after the chart finished loading. And here you can use this to refer to the chart :
$('#container').highcharts({
chart: {
events: {
load: function() {
var extremeY = this.yAxis[0].getExtremes();
var extremeX = this.xAxis[0].getExtremes();
var lineSeries = {
type: 'line',
data: [
[extremeX.min, extremeY.min],
[extremeX.max, extremeY.max]
],
lineWidth: 0.5,
lineColor: 'rgb(0,0,0)',
marker: {
enabled: false
}
};
this.addSeries(lineSeries);
}
},
//...
});
Here is the new updated fiddle
Since you need only a diagonal path, then you could add it using renderer
Example: http://jsfiddle.net/cr7gq4st/
function diagonal() {
var chart = this,
ren = chart.renderer,
diag = chart.diag,
attrs = {
'stroke-width': 0.5,
stroke: '#000',
zIndex: 1
},
topR = {
x: chart.plotLeft + chart.plotWidth,
y: chart.plotTop
},
bottomL = {
x: chart.plotLeft,
y: chart.plotTop + chart.plotHeight
},
d = 'M ' + bottomL.x + ' ' + bottomL.y + ' L ' + topR.x + ' ' + topR.y;
if( !diag ) { //if doesn't exist, then create
chart.diag = ren.path().attr(attrs).add();
}
chart.diag.attr({d:d});
}
I'm trying to set the min zoom (max range) of my chart. Basically I'm trying to do the opposite of the minRange property. I'm struggling for a while with this problem. I have a "solution", but I don't like it, this solution allow the user to choose a range greater then the "max range", and immediately correct it.
POSSIBLE SOLUTION
$(function() {
var lastMin;
var lastMax;
var maxRange = 12 * 30 * 24 * 3600 * 1000; //12 month
$('#container').highcharts('StockChart', {
scrollbar: {
liveRedraw: false
},
xAxis: {
events: {
afterSetExtremes: function(e) {
var max = this.max,
min = this.min;
if (lastMin && lastMax) {
if(max-min > maxRange) {
if (min < lastMin) {
min = max - maxRange;
} else {
max = min + maxRange;
}
}
}
var x = this;
setTimeout(function(){
x.setExtremes(min,max); //chart xAxis
}, 1);
lastMin = min;
lastMax = max;
}
}
},
rangeSelector: {
selected: 1
},
series: [{
name: 'USD to EUR',
data: usdeur
}]
});
});
I want to block the user from choosing a range greater than the allowed, in other words, block the navigator when it's too big
I'm also following this issue, I tried all the proposed solution, but I'm having errors ("Uncaught ReferenceError: Highcharts is not defined")
Thanks Sebastian!
I managed to find a solution (fiddle) wrapping the "render" function. Doing that I managed to really set a "min zoom" on the navigator bar.
$(function() {
var lastX0;
var lastX1;
var maxRange = 100; //100 pixels
(function (H) {
H.wrap(H.Scroller.prototype, 'render', function (proceed) {
console.log(arguments)
if(arguments[4] - arguments[3] > maxRange + 2) {
if (arguments[3] < lastX0) {
arguments[3] = lastX0;
} else {
arguments[4] = lastX1;
}
}
proceed.apply(this, [].slice.call(arguments, 1));
lastX0 = arguments[3];
lastX1 = arguments[4];
});
}(Highcharts));
$('#container').highcharts('StockChart', {
scrollbar: {
liveRedraw: true
},
series: [{
name: 'USD to EUR',
data: usdeur
}]
});
var highchart = $('#container').highcharts();
var extremes = highchart.xAxis[0].getExtremes();
var rangeTotal = extremes.max - extremes.min;
var f = maxRange / $('#container').width();
highchart.xAxis[0].setExtremes(extremes.max - (f * rangeTotal), extremes.max);
});
In the sample code I'm used a fixed amount of pixels, but in my real application i'm making it dynamic. I making this, because I can't use the data grouping property in the software that I'm working, and since the minimum size of a bar in a chart is 1 pixel (obviously) highcharts hide some bars (or points).
I'm setting the minimum zoom so all bar in the displayed range are visible , since the user can't display a higher range in the x Axis the "hidden" bar "problem" (is an awesome feature, but I can't make use of it) won't happen
I build a chart like the lazy loading example (http://www.highcharts.com/stock/demo/lazy-loading) but with having the input fields of the range selector.
When I zoom in everything is fine. Now I would like to change the selected range by using the input fields.
But I am not able to change the input to values beyond the zoomed area despite the fact I have more data visible in my navigator.
In the setExtremes function I am doing some calculations:
DX.IPFixGraphModule.prototype.setExtremes = function(e) {
var fromTime,
maxZoom = 30 * 60 * 1000,
now = new Date().getTime();
if (e.min) {
fromTime = e.min;
} else {
fromTime = e.dataMin;
}
fromTime = Math.round(fromTime);
var diff = now - fromTime;
// -1 month = max 1 week zoom
if (diff >= 2592000000) {
maxZoom = 7 * 24 * 60 * 60 * 1000;
}
// -1 week = max 12 hour zoom
else if (diff >= 604800000) {
maxZoom = 12 * 60 * 60 * 1000;
}
// this refers to axis
// #see http://api.highcharts.com/highstock#Axis.update
this.update({
minRange: maxZoom
}, false);
};
But the values I receive in e.min and e.max are not the original input values but already corrected to the displayed time range.
// handle changes in the input boxes
input.onchange = function () {
var inputValue = input.value,
value = (options.inputDateParser || Date.parse)(inputValue),
xAxis = chart.xAxis[0],
dataMin = xAxis.dataMin,
dataMax = xAxis.dataMax;
// If the value isn't parsed directly to a value by the browser's Date.parse method,
// like YYYY-MM-DD in IE, try parsing it a different way
if (isNaN(value)) {
value = inputValue.split('-');
value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
}
if (!isNaN(value)) {
// Correct for timezone offset (#433)
if (!defaultOptions.global.useUTC) {
value = value + new Date().getTimezoneOffset() * 60 * 1000;
}
// Validate the extremes. If it goes beyound the data min or max, use the
// actual data extreme (#2438).
if (isMin) {
if (value > rangeSelector.maxInput.HCTime) {
value = UNDEFINED;
} else if (value < dataMin) {
value = dataMin;
}
} else {
if (value < rangeSelector.minInput.HCTime) {
value = UNDEFINED;
} else if (value > dataMax) {
value = dataMax;
}
}
// Set the extremes
if (value !== UNDEFINED) {
chart.xAxis[0].setExtremes(
isMin ? value : xAxis.min,
isMin ? xAxis.max : value,
UNDEFINED,
UNDEFINED,
{ trigger: 'rangeSelectorInput' }
);
}
}
};
(Code taken from highstock.src.js around line 21126)
So I cannot extend my zoom beyond the current active selection, but the navigator displays more data.
Does anyone know a way to set a date beyond the currently zoomed time range?
Possible Solution
I solved it by checking the navigator range in the "afterSetExtremes" Event.
DX.IPFixGraphModule.prototype.afterSetExtremes = function(e) {
if (e.trigger === 'rangeSelectorInput') {
var fromValue = this.stockchart.rangeSelector.minInput.value,
toValue = this.stockchart.rangeSelector.maxInput.value,
fromTime = parseDateTime(fromValue),
toTime = parseDateTime(toValue),
navigatorAxis = this.stockchart.get('navigator-x-axis'),
maxValue = navigatorAxis.dataMax,
minValue = navigatorAxis.dataMin;
if (fromTime < minValue) {
fromTime = minValue;
}
if (toTime > maxValue) {
toTime = maxValue;
}
this.stockchart.xAxis[0].setExtremes(fromTime, toTime);
} else {
var fromTime,
toTime;
if (e.min) {
fromTime = e.min;
} else {
fromTime = e.dataMin;
}
fromTime = Math.round(fromTime);
if (e.max) {
toTime = e.max;
} else {
toTime = e.dataMax;
}
toTime = Math.round(toTime);
this.settings.afterSetExtremes({startTimestamp: fromTime, endTimestamp: toTime});
}
};
Or see solution below and override the method.
There is no default API for that. Extend Highcharts via overriding drawInput function (your second code snippet).
There is a part of code that you should comment out or remove - the if clause after:
// Validate the extremes. If it goes beyound the data min or max, use the
// actual data extreme (#2438).
Example: http://jsfiddle.net/epL7awo4/1/
$(function () {
(function (H) {
H.wrap(H.RangeSelector.prototype, 'drawInput', function (proceed) {
var name = arguments[1],
merge = H.merge,
createElement = H.createElement,
PREFIX = 'highcharts-',
ABSOLUTE = 'absolute',
PX = 'px',
extend = H.extend,
pInt = H.pInt,
UNDEFINED;
//drawInput: function (name) {
var rangeSelector = this,
chart = rangeSelector.chart,
chartStyle = chart.renderer.style,
renderer = chart.renderer,
options = chart.options.rangeSelector,
defaultOptions = H.getOptions(),
lang = defaultOptions.lang,
div = rangeSelector.div,
isMin = name === 'min',
input,
label,
dateBox,
inputGroup = this.inputGroup;
// Create the text label
this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
.attr({
padding: 2
})
.css(merge(chartStyle, options.labelStyle))
.add(inputGroup);
inputGroup.offset += label.width + 5;
// Create an SVG label that shows updated date ranges and and records click events that
// bring in the HTML input.
this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset)
.attr({
padding: 2,
width: options.inputBoxWidth || 90,
height: options.inputBoxHeight || 17,
stroke: options.inputBoxBorderColor || 'silver',
'stroke-width': 1
})
.css(merge({
textAlign: 'center',
color: '#444'
}, chartStyle, options.inputStyle))
.on('click', function () {
rangeSelector.showInput(name); // If it is already focused, the onfocus event doesn't fire (#3713)
rangeSelector[name + 'Input'].focus();
})
.add(inputGroup);
inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
// Create the HTML input element. This is rendered as 1x1 pixel then set to the right size
// when focused.
this[name + 'Input'] = input = createElement('input', {
name: name,
className: PREFIX + 'range-selector',
type: 'text'
}, extend({
position: ABSOLUTE,
border: 0,
width: '1px', // Chrome needs a pixel to see it
height: '1px',
padding: 0,
textAlign: 'center',
fontSize: chartStyle.fontSize,
fontFamily: chartStyle.fontFamily,
top: chart.plotTop + PX // prevent jump on focus in Firefox
}, options.inputStyle), div);
// Blow up the input box
input.onfocus = function () {
rangeSelector.showInput(name);
};
// Hide away the input box
input.onblur = function () {
rangeSelector.hideInput(name);
};
// handle changes in the input boxes
input.onchange = function () {
var inputValue = input.value,
value = (options.inputDateParser || Date.parse)(inputValue),
xAxis = chart.xAxis[0],
dataMin = xAxis.dataMin,
dataMax = xAxis.dataMax;
// If the value isn't parsed directly to a value by the browser's Date.parse method,
// like YYYY-MM-DD in IE, try parsing it a different way
if (isNaN(value)) {
value = inputValue.split('-');
value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
}
if (!isNaN(value)) {
// Correct for timezone offset (#433)
if (!defaultOptions.global.useUTC) {
value = value + new Date().getTimezoneOffset() * 60 * 1000;
}
// Validate the extremes. If it goes beyound the data min or max, use the
// actual data extreme (#2438).
/* if (isMin) {
if (value > rangeSelector.maxInput.HCTime) {
value = UNDEFINED;
} else if (value < dataMin) {
value = dataMin;
}
} else {
if (value < rangeSelector.minInput.HCTime) {
value = UNDEFINED;
} else if (value > dataMax) {
value = dataMax;
}
}*/
// Set the extremes
if (value !== UNDEFINED) {
chart.xAxis[0].setExtremes(
isMin ? value : xAxis.min,
isMin ? xAxis.max : value,
UNDEFINED,
UNDEFINED, {
trigger: 'rangeSelectorInput'
});
}
}
};
//},
});
}(Highcharts));
/**
* Load new data depending on the selected min and max
*/
function afterSetExtremes(e) {
var chart = $('#container').highcharts();
chart.showLoading('Loading data from server...');
$.getJSON('http://www.highcharts.com/samples/data/from-sql.php?start=' + Math.round(e.min) +
'&end=' + Math.round(e.max) + '&callback=?', function (data) {
chart.series[0].setData(data);
chart.hideLoading();
});
}
// See source code from the JSONP handler at https://github.com/highslide-software/highcharts.com/blob/master/samples/data/from-sql.php
$.getJSON('http://www.highcharts.com/samples/data/from-sql.php?callback=?', function (data) {
// Add a null value for the end date
data = [].concat(data, [
[Date.UTC(2011, 9, 14, 19, 59), null, null, null, null]
]);
// create the chart
$('#container').highcharts('StockChart', {
chart: {
type: 'candlestick',
zoomType: 'x'
},
navigator: {
adaptToUpdatedData: false,
series: {
data: data
}
},
scrollbar: {
liveRedraw: false
},
title: {
text: 'AAPL history by the minute from 1998 to 2011'
},
subtitle: {
text: 'Displaying 1.7 million data points in Highcharts Stock by async server loading'
},
rangeSelector: {
buttons: [{
type: 'hour',
count: 1,
text: '1h'
}, {
type: 'day',
count: 1,
text: '1d'
}, {
type: 'month',
count: 1,
text: '1m'
}, {
type: 'year',
count: 1,
text: '1y'
}, {
type: 'all',
text: 'All'
}],
inputEnabled: true,
selected: 4 // all
},
xAxis: {
events: {
afterSetExtremes: afterSetExtremes
},
minRange: 3600 * 1000 // one hour
},
yAxis: {
floor: 0
},
series: [{
data: data,
dataGrouping: {
enabled: false
}
}]
});
});
});