I would to print into my charts the hours and minutes, for example "development: 27 h 30 m".
Now I have this:
In my script "data" represents "hours"
{
name: value.blank? ? "other" : value,
data: all_weeks.merge(data_weeks.to_h).values.map do |data_value|
data_value.nil? ? 0 : ( data_value.to_f / 60 ).round(2)
end
}
....
f.tooltip(
pointFormat: "<span style='color:{series.color}'>{series.name}</span>: <b>{point.y} h</b><br/>",
split: true
)
I have tried to set point.y into pointFormat but no have effects. ( Edit: pointFormat would a generic string! )
How can I solve? Thanks in advance.
Edit:
I solved adding:
LazyHighCharts::HighChart.new("container") do |f|
f.tooltip(
formatter: "\
function() { \
var s = []; \
s.push(this.x); \
this.points.forEach(function(point) { \
s.push('<b>' + point.series.name + '</b>: ' + Math.floor(point.y) + ' h ' + Math.floor((point.y % 1) * 60) + ' m'); \
}); \
return s; \
}".js_code,
split: true
)
....
end
Maybe you could introduce two variables, data_hours and data_minutes, and write a string interpolation with theses variables.
data_hours = data_value.to_i
data_minutes = ((data_value - data_hours) * 60).to_i
data_to_display = "#{data_hours} h #{data_minutes} m"
Hope it will help !
Written in 0.12 hours ;)
["dev: 27.5 h", "dev: 22 h", "dev: 0.3h"].map do |s|
s.gsub(/\d+(\.\d+)?\s*h/i) do |h|
h, m = h.to_f.divmod(1)
[
("#{h} h" unless h.zero?),
("#{(60 * m).round} m" unless m.zero?)
].compact.join(" ")
end
end
#⇒ ["dev: 27 h 30 m", "dev: 22 h", "dev: 18 m"]
Related
Problem:
The nodeFormat works well for sum (point.sum), but does not work for percentage (point.percentage).
Working nodeFormat:
nodeFormat: '{point.name} {point.sum}
Not working nodeFormat:
nodeFormat: '{point.name} {point.percentage}%'
Expected output:
China >> 147 (100)%
US >> 53 (36)%
EU >> 123 (84)%
Formula for expected output:
dataSum = 147
chinaPercentage = chinaData/dataSum*100 = 147/147*100% = 100%
usPercentage = usData/dataSum*100 = 53/147*100% = 36%
euPercentage = euData/dataSum*100 = 84/147*100% = 84%
Fiddle reference:
https://jsfiddle.net/8L75ez9n/
You need to use nodeFormatter function and calculate percentage value:
dataLabels: {
nodeFormatter: function() {
const point = this.point;
const percentage = point.linksTo[0] ?
point.sum / point.linksTo[0].fromNode.sum * 100 :
100;
return point.name + ' >> ' + point.sum + ' (' + Math.round(percentage) + ')%'
}
}
Live demo: https://jsfiddle.net/BlackLabel/q845s7on/
API Reference: https://api.highcharts.com/highcharts/series.sankey.dataLabels.nodeFormatter
I have Highcharts chart rendered on yAxis[0] and whenever the user selects an indicator like Aroon Oscillator, MACD etc. it is added as a new yAxis in the rendering container. Example:
The above snapshot is with the Aroon Oscillator. All good.
When i select Slow Stochastic though i get the following:
First of all both SS$K and SS$D have the same value as you can see in the tooltip which is clearly wrong and both lines have the same color for a reason when they should not because:
I tried to log the p.series.yData[p.point.index] (the currently hovered point yData value) in my console and for my surprise I get the following:
It returns an array (but why) of 2 values, and both plots access the first value, instead of SS%D accessing the second value. This is a Slow Stochastic problem from Highcharts since the Aroon Oscillator which has the same philosophy of many plots on the same yAxis, still works fine as you can see in the first picture (where the lines have the correct color and correct values assigned).
Lastly as you can see with another indicators MACD (which has the same issue as mentioned before with Slow Stochastic) - and VPT which works as intended, when i go full range, those have a gap at start. Why is that happening?
Has anyone come across a similar indicator problem from Highcharts and if so, could anyone give me his/her lights on how to solve the problem?
Thanks in advance, Christopher.
P.S. Those are my tooltip options for highcharts:
tooltip: {
formatter: function() {
let finalDate = '';
let tooltipString = '';
$.each(this.points, function(i, p) {
// console.log(p.series.name, p.series.yData[p.point.index]);
//Starting date
let dateTo = new Date(p.x),
month = '' + (dateTo.getMonth() + 1),
day = '' + dateTo.getDate(),
year = dateTo.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
let intradayTime = '';
if (p.point.isIntradayData) {
let hours = dateTo.getHours();
let minutes = ('0' + dateTo.getMinutes()).slice(-2);
intradayTime += ' ' + hours + ':' + minutes;
}
dateTo = [day, month, year].join('.');
//If we wanna display previous date too:
let dateFrom = '';
if (p.point.previousDate) {
dateFrom = new Date(p.point.previousDate);
month = '' + (dateFrom.getMonth() + 1);
day = '' + dateFrom.getDate();
year = dateFrom.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
dateFrom = [day, month, year].join('.');
dateFrom += ' - ';
}
finalDate = dateFrom + dateTo + intradayTime;
if (p.series.userOptions.compare == 'percent') {
tooltipString +=
'<br>' +
`<span style="color:${p.color}; font-size:10px; font-family:\'Arial\'">` +
p.series.name +
': </span>' +
'<span style="font-size:10px; font-family:\'Arial\'">' +
' ' +
p.point.percentageDifference +
`%(${p.y})`;
('</span>');
} else if (
p.series.type == 'candlestick' ||
p.series.type == 'ohlc'
) {
tooltipString +=
'<br>' +
`<span style="color:${p.color}; font-size:10px; font-family:\'Arial\'">` +
p.series.name +
': </span>' +
'<br>' +
'<span style="font-size:10px; font-family:\'Arial\'">' +
'Open: ' +
p.point.open +
'<br>' +
'<span style="font-size:10px; font-family:\'Arial\'">' +
'High: ' +
p.point.high +
'<br>' +
'<span style="font-size:10px; font-family:\'Arial\'">' +
'Low: ' +
p.point.low +
'<br>' +
'<span style="font-size:10px; font-family:\'Arial\'">' +
'Close: ' +
p.point.close +
'</span> <br>';
} else {
let pointValueFormatted = p.y.toFixed(2);
tooltipString +=
'<br>' +
`<span style="color:${p.color}; font-size:10px; font-family:\'Arial\'">` +
p.series.name +
': </span>' +
'<span style="font-size:10px; font-family:\'Arial\'">' +
' ' +
pointValueFormatted +
'</span>';
}
});
return (
'<span style=" font-size:10px; font-family:\'Arial\'">' +
finalDate +
'</span>' +
tooltipString
);
},
borderColor: '#7fb7f0',
shared: true,
},
INDICATORS LOGIC
It's impossible to reproduce it in jsfiddle due to the size, the complexity and for security reasons due to policies. So a user can select indicators from a tabmenu like:
and they way the indicators are sorted:
//Sorting indicators.
sortIndicators() {
console.log(`Function Call: sortIndicators()`);
this.cleanIndicators(); // Just clearing the chart yaxises and the indicators array list to iterate over indicators again.
let counter = 0;
//For every indicator
for (let indicator of this.indicators) {
//If it is chart indicator we render it on yAxis[0] with our prices
if (toolsManager.isChartIndicator(indicator)) {
//some indicators have multiple arrays of data instead of one saved in an array called arrayOfObj
if (indicator.arrayOfObj) {
for (let arg of indicator.arrayOfObj) {
this.highchartOptions.series.push(arg);
}
} else {
this.highchartOptions.series.push(indicator);
}
} else {
//If it not chart indicator
this.highchartOptions.chart.height +=
2 * this.yAxisMargin + this.yAxisHeight;
let topCalculation =
456 + (counter + 1) * this.yAxisMargin + counter * this.yAxisHeight;
//yAxis to add
let addedAxis = {
title: {
enabled: false,
},
opposite: true,
min: null,
height: this.yAxisHeight,
top: topCalculation + this.yAxisMargin,
showLastLabel: false,
labels: {
enabled: true,
x: -30,
y: 0,
style: {
fontSize: '10px',
fontFamily: 'Arial',
color: '#999999',
},
},
};
this.highchartOptions.yAxis.push(addedAxis);
if (indicator.arrayOfObj) {
//some indicators have multiple arrays of data instead of one saved in an array called arrayOfObj
for (let arg of indicator.arrayOfObj) {
//for every array of that arrayOfObj
arg.yAxis = counter + 1;
this.highchartOptions.series.push(arg); //push it to series
}
} else {
indicator.yAxis = counter + 1;
if (indicator.id === 'volume') {
indicator.data = this.volumeSeries;
}
this.highchartOptions.series.push(indicator);
}
counter++;
}
}
},
the part where the magic happens is here:
if (indicator.arrayOfObj) {
//some indicators have multiple arrays of data instead of one saved in an array called arrayOfObj
for (let arg of indicator.arrayOfObj) {
//for every array of that arrayOfObj
arg.yAxis = counter + 1;
this.highchartOptions.series.push(arg); //push it to series
}
} else {
indicator.yAxis = counter + 1;
if (indicator.id === 'volume') {
indicator.data = this.volumeSeries;
}
the reason it is implemented like this is because when an indicator is selected an object is returned from the server which may have one of the two following structures:
data structure 1:
data structure 2:
And after sorting out what type of structure I am dealing with, I am pushing it into the series to display. counter variable is just used for the y-axis's. Arron Oscillator and Slow Stochastic both fulfill the first part of the conditional, because both return an object which contains and arrayOfObj (for the line colors etc as you can see in the photo uploaded).
I'm working with an ApplicationHelper method that translates Time objects into 'humanized' time measurements in my view:
def humanize_seconds s
if s.nil?
return ""
end
if s > 0
m = (s / 60).floor
s = s % 60
h = (m / 60).floor
m = m % 60
d = (h / 24).floor
h = h % 24
w = (d / 7).floor
d = d % 7
y = (w / 52).floor
w = w % 52
output = pluralize(s, "second") if (s > 0)
output = pluralize(m, "minute") + ", " + pluralize(s, "second") if (m > 0)
output = pluralize(h, "hour") + ", " + pluralize(m, "minute") if (h > 0)
output = pluralize(d, "day") + ", " + pluralize(h, "hour") if (d > 0)
output = pluralize(w, "week") + ", " + pluralize(d, "day") if (w > 0)
output = pluralize(y, "years") + ", " + pluralize(w, "week") if (y > 0)
return output
else
return pluralize(s, "second")
end
end
It's working great, but I'm running into an issue when translating the end result of a method designed to list time intervals in a specified location:
RFIDTag.rb:
def time_since_first_tag_use
product_selections.none? ? "N/A" : Time.now - product_selections.order(staged_at: :asc).first.staged_at
end
Product.rb:
def first_staged_tag
rfid_tags.map { |rfid| rfid.time_since_first_tag_use.to_i }.join(", ")
end
View: (html.erb):
Putting the value out there works, and lists the values as first_staged_tag is meant to do, but it only does so in seconds:
<% #products.order(created_at: :desc).each do |product| %>
<td><%= product.name %></td> #Single product name
<td><%= product.first_staged_tag %></td> list, i.e. #40110596, 40110596, 39680413, 39680324
<%end%>
While converting in the usual way <td><%= humanize_seconds(product.first_staged_tag) %></td>, as has worked for single values gives this error:
comparison of String with 0 failed
Extracted source (around line #88):
86 return ""
87 end
88 if s > 0
89 m = (s / 60).floor
90 s = s % 60
91 h = (m / 60).floor
Meanwhile, trying to apply that method in the Product model first_staged_tag method generates a NoMethod error on humanize_seconds. How can I get my list of times to recognize the time conversion?
An iteration of everything tried is in the comments.
Solved! The tags had to be mapped in the Product model, and converted there:
#Product.rb
def first_staged
rfid_tags.map { |rfid| rfid.time_since_first_tag_use.to_i }
end
Then iterated over again in the view as a whole:
<%= product.first_staged.map {|time| humanize_seconds(time) }.join(", ") %>
I've created custom formatter function for highstocks.js:
var tooltip = [];
for (key in this.points) {//if point type is portfolio
if ( this.points[key].point.type == 'portfolio' ) {
tooltip[key] = '<span style="color:' + his.points[key].series.color +'">' + this.points[key].series.name + '</span>' + '<br/><b>'+ _('', 'Net assets: ') + _s(this.points[key].point.sum, 2) + '</b>' + '<br/><b>'+ _('', 'Чистые активы: ') + _s(this.points[key].point.netassets, 2) +'</b> (<span style="color:' + ( (this.points[key].point.change < 0 )?'#b86565':'#619000') +'">' + _s(this.points[key].point.change, 0) +'%</span>)<br/>';
} else {
tooltip[key] = '<span style="color:' + this.points[key].series.color +'">' + this.points[key].series.name + '</span>: <b>'+ _s(this.points[key].point.y, 2) +'</b> (<span style="color:' + ( (this.points[key].point.change < 0 )?'#b86565':'#619000') +'">' + _s(this.points[key].point.change, 0) +'%</span>)<br/>';
}
}
var tl = '';
for (key in tooltip) {
tl += tooltip[key]
}
var date = Highcharts.dateFormat('%d %b %Y', this.points[0].point.x);
tl = date + '<br/>' +tl;
return tl;
The feature is that this function usues not only Y of a point, but also some additinal properties, that I have added to the point: such as type.
For points that are "portfolio" type the tooltop shoold be rendered differently and has to have much more data then for "regular" point type.
The problem that I've encountered that when conatiner div has small width, my template doesnt work, although it works fine when div's width is big.
Highstocks.js does default aggregation when renders chart to relativly small area: http://api.highcharts.com/highstock#plotOptions.area.dataGrouping
When points are groupped, they lose all additional attributes, leaving only Y property, so complex tooltop wont work.
To fix it I had to disable data groupping in chart options:
plotOptions: {
series: {
dataGrouping: {
enabled: false
}
}
},
Is ther a way to display complex tooltip on small chart without disabling dataGrouping?
The question is: 'How to group point.type'?
I guess you would like to group point by type, and then display n-points in that place? Or group point as is, but in options count number of types? What if user will define myCustomFancyProperty - what then? Aggregate all own properties from point? It's getting harder and harder.. what can I advice is to create an idea here with some explanation/solution.
You can always get from grouped point x-value (this.x), and then loop over all points (accessible via this.points[0].series.options.data), find the closest point to that timestamp and display required value.
as Pawel Fus suggested, I loop throught all points to find closest value and add additinal attributes for each groupped point.
Here is th whole formmater function.
formatter: function() {
var tooltip = [];
//Тултип
for (var key in this.points) {//Если точка - пользователь
//прочитаем тип
var closestX = 0;
for (var j in this.points[key].series.options.data ) {
if ( Math.abs(this.points[key].series.options.data[j].x - this.points[key].point.x) < diff ) {
closestX = j;
}
var diff = Math.abs(this.points[key].series.options.data[j].x - this.points[key].point.x);
}
this.points[key].point.type = this.points[key].series.options.data[closestX].type;
this.points[key].point.sum = this.points[key].series.options.data[closestX].sum;
this.points[key].point.netassets = this.points[key].series.options.data[closestX].netassets;
if ( this.points[key].point.type == 'portfolio' ) { //если точка не пользователь
tooltip[key] = '<span style="color:' + this.points[key].series.color +'">' + this.points[key].series.name + '</span>' +
'<br/><b>'+ _('', 'Сумма активов: ') + _s(this.points[key].point.sum, 2) + '</b>' +
'<br/><b>'+ _('', 'Чистые активы: ') + _s(this.points[key].point.netassets, 2) +'</b> (<span style="color:' + ( (this.points[key].point.change < 0 )?'#b86565':'#619000') +'">' + _s(this.points[key].point.change, 0) +'%</span>)<br/>';
} else {
tooltip[key] = '<span style="color:' + this.points[key].series.color +'">' + this.points[key].series.name + '</span>: <b>'+ _s(this.points[key].point.y, 2) +'</b> (<span style="color:' + ( (this.points[key].point.change < 0 )?'#b86565':'#619000') +'">' + _s(this.points[key].point.change, 0) +'%</span>)<br/>';
}
}
var tl = '';
for (key in tooltip) {
tl += tooltip[key]
}
//Дата
var date = Highcharts.dateFormat('%d %b %Y', this.points[0].point.x);
tl = date + '<br/>' +tl;
return tl;
},
I am using a stock chart to show trend data. On the backend I am getting what the valueSuffix should be (or valuePrefix as the case may be). I am also formatting the date display in the tooltip. Here is the important part of the series declaration:
...
name: 'Wages',
tooltip: {
valuePrefix: '$',
valueDecimals: 0
},
...
Here is the tooltip formatter:
...
tooltip: {
formatter: function () {
var s = '<b>';
if (Highcharts.dateFormat('%b', this.x) == 'Jan') {
s = s + 'Q1';
}
if (Highcharts.dateFormat('%b', this.x) == 'Apr') {
s = s + 'Q2';
}
if (Highcharts.dateFormat('%b', this.x) == 'Jul') {
s = s + 'Q3';
}
if (Highcharts.dateFormat('%b', this.x) == 'Oct') {
s = s + 'Q4';
}
s = s + ' ' + Highcharts.dateFormat('%Y', this.x) + '</b>';
$.each(this.points, function (i, point) {
s += '<br/><span style="color: ' + point.series.color + '">' + point.series.name + ':</span>' + point.y;
});
return s;
}
}
...
Example jsFiddle.
If you notice the prefix for the dollar sign is not showing on the Wage series. I am not really sure what I am missing here.
The fix is to break up the label formatting and the value formatting into distinct sections. See example jsFiddle.
Set the chart.tooltip like:
...
tooltip: {
headerFormat: '<b>{point.key}</b><br>',
xDateFormat: '%Q'
},
...
On the xAxis I replaced the label formatter with:
...
format: '{value: %Q}'
...
Inside the series I kept my suffix/prefix/decimals the same:
...
tooltip: {
valuePrefix: '$',
valueDecimals: 0
},
...
The big change came when I found that you can set your own date format label. I created one for Quarters (which is what I did the original cumbersome code for):
Highcharts.dateFormats = {
Q: function (timestamp) {
var date = new Date(timestamp);
var y = date.getFullYear();
var m = date.getMonth() + 1;
if (m <= 3) str = "Q1 " + y;
else if (m <= 6) str = "Q2 " + y;
else if (m <= 9) str = "Q3 " + y;
else str = "Q4 " + y;
return str;
}
};
I now get the tooltip to show the correct labels.
Unless something has changed in a recent version, you can't set your tooltip options within the series object. You can set anything that you would set in the plotOptions on the series level, but not the tooltip.
You can set a check within your main tooltip formatter to check for the series name, and apply the prefix accordingly.
{{edit::
This appears to be an issue of the formatter negating the valuePrefix setting.
Old question... but I spent hours looking for an acceptable way to do this.
/!\ : Before OP's answer...
If someone is looking for a way to use the formatter provided by highcharts (pointFormat attribute) (Doc here: LABELS AND STRING FORMATTING), you will find a (bad) example here :
http://jsfiddle.net/gh/get/jquery/1.7.2/highslide-software/highcharts.com/tree/master/samples/highcharts/tooltip/pointformat/
Why "bad" ? because if you change :
pointFormat: '{series.name}: <b>{point.y}</b><br/>',
with :
pointFormat: '{series.name}: <b>{point.y:,.1f}</b><br/>',
you add thousand separators (useless here) and force number format to 1 decimal ... and loose the suffix.
The answer is : series.options.tooltip.valueSuffix (or series.options.tooltip.valuePrefix)
Example :
pointFormat: '<span style="color:{series.color}">\u25CF</span> {series.name}: <b>{point.y:,.0f}{series.options.tooltip.valueSuffix}</b><br/>',
Hope this will save you some time.
Now OP's Answer :
Based on what I said, you can change :
$.each(this.points, function (i, point) {
s += '<br/><span style="color: ' + point.series.color + '">' + point.series.name + ':</span>' + point.y;
});
For this :
$.each(this.points, function (i, point) {
s += '<br/><span style="color: ' + point.series.color + '">'
+ point.series.name + ':</span>'
+ (typeof point.series.options.tooltip.valuePrefix !== "undefined" ?
point.series.options.tooltip.valuePrefix : '')
+ point.y
+ (typeof point.series.options.tooltip.valueSuffix !== "undefined" ?
point.series.options.tooltip.valueSuffix : '');
});
This will add prefix and/or suffix if they exists