Highcharts.js question: is it possible to not draw line beyond MACD indicator histogram? - highcharts

I develop a project using Highstock.js library. I have a requirement to not draw lines beyond MACD indicator histogram (https://prnt.sc/lxjoit). In Highstock.js documentation related to MACD indicator there is macdLine API: https://api.highcharts.com/highstock/series.macd.macdLine , signalLine API: https://api.highcharts.com/highstock/series.macd.signalLine.zones. Those API allows only to set colors for MACD indicator parts divided by y axis. So it doesn't fit. From documentation it isn't clear if we can to not draw MACD indicator lines beyond the histogram. Do you know if it is possible to implement that and how? Please advise.
Here is live demo in JSFiddle showing chart with MACD indicator: http://jsfiddle.net/ogorobets/x3tcpq72/14/
var ohlc = JSON.parse(ohlcStringified),
volume = JSON.parse(volumeStringified);
var wvapSerieData = [];
var lastDayDate = new Date("December 6, 2018 00:00:00");
var lastDayDateTs = lastDayDate.getTime();
Highcharts.stockChart('container', {
chart: {
borderWidth: 1
},
title: {
text: 'Volume Weighted Average Price (VWAP)'
},
legend: {
enabled: true
},
yAxis: [{
height: '30%'
}, {
top: '30%',
height: '35%',
offset: 0
}, {
top: '65%',
height: '35%',
offset: 0
}],
series: [{
type: 'candlestick',
id: 'candlestick',
name: 'AAPL',
data: ohlc,
tooltip: {
valueDecimals: 2
}
}, {
type: 'column',
id: 'volume',
name: 'Volume',
data: volume,
yAxis: 1
},
{
type: 'macd',
color: '#f05f5f',
linkedTo: 'candlestick',
showInLegend: true,
enableMouseTracking: true,
dataGrouping: {
enabled: false,
},
zones:[
{
value: 0,
color: '#f05f5f',
},
{
color: '#31c26d'
}
],
yAxis: 2
}]
});

Unfortunately, MACD indicator was not designed to not plot MACD indicator lines beyond the histogram.
However, you can override the method that calculates MACD values and remove first values that are beyond the histogram. Check the code and demo I posted you below.
Lines added to H.seriesTypes.macd.prototype.getValues method:
// params.signalPeriod - 1 - amount of points beyond the histroram
MACD.splice(0, params.signalPeriod - 1);
xMACD.splice(0, params.signalPeriod - 1);
yMACD.splice(0, params.signalPeriod - 1);
Whole wrapper code:
(function(H) {
H.seriesTypes.macd.prototype.getValues = function(series, params) {
var j = 0,
EMA = H.seriesTypes.ema,
merge = H.merge,
defined = H.defined,
correctFloat = H.correctFloat,
MACD = [],
xMACD = [],
yMACD = [],
signalLine = [],
shortEMA,
longEMA,
i;
if (series.xData.length < params.longPeriod + params.signalPeriod) {
return false;
}
// Calculating the short and long EMA used when calculating the MACD
shortEMA = EMA.prototype.getValues(series, {
period: params.shortPeriod
});
longEMA = EMA.prototype.getValues(series, {
period: params.longPeriod
});
shortEMA = shortEMA.values;
longEMA = longEMA.values;
// Subtract each Y value from the EMA's and create the new dataset
// (MACD)
for (i = 1; i <= shortEMA.length; i++) {
if (
defined(longEMA[i - 1]) &&
defined(longEMA[i - 1][1]) &&
defined(shortEMA[i + params.shortPeriod + 1]) &&
defined(shortEMA[i + params.shortPeriod + 1][0])
) {
MACD.push([
shortEMA[i + params.shortPeriod + 1][0],
0,
null,
shortEMA[i + params.shortPeriod + 1][1] -
longEMA[i - 1][1]
]);
}
}
// Set the Y and X data of the MACD. This is used in calculating the
// signal line.
for (i = 0; i < MACD.length; i++) {
xMACD.push(MACD[i][0]);
yMACD.push([0, null, MACD[i][3]]);
}
// Setting the signalline (Signal Line: X-day EMA of MACD line).
signalLine = EMA.prototype.getValues({
xData: xMACD,
yData: yMACD
}, {
period: params.signalPeriod,
index: 2
});
signalLine = signalLine.values;
// Setting the MACD Histogram. In comparison to the loop with pure
// MACD this loop uses MACD x value not xData.
for (i = 0; i < MACD.length; i++) {
if (MACD[i][0] >= signalLine[0][0]) { // detect the first point
MACD[i][2] = signalLine[j][1];
yMACD[i] = [0, signalLine[j][1], MACD[i][3]];
if (MACD[i][3] === null) {
MACD[i][1] = 0;
yMACD[i][0] = 0;
} else {
MACD[i][1] = correctFloat(MACD[i][3] -
signalLine[j][1]);
yMACD[i][0] = correctFloat(MACD[i][3] -
signalLine[j][1]);
}
j++;
}
}
MACD.splice(0, params.signalPeriod - 1);
xMACD.splice(0, params.signalPeriod - 1);
yMACD.splice(0, params.signalPeriod - 1);
return {
values: MACD,
xData: xMACD,
yData: yMACD
};
}
})(Highcharts);
Demo:
http://jsfiddle.net/1f2m0yz4/
Docs:
https://www.highcharts.com/docs/extending-highcharts/extending-highcharts

Related

HIGHCHARTS GANTT / X labels not rendering, not rotating, not overlapping

I have a chart with 3 X axes, the first show the year, the next line shows the Quarter (3 months) and the third by month, the expected behavior is similar to the following:
Now I do understand that because of the space, some of them disappear, but I tried to use allowOverlap and nothing happnes, rotate 90 or -90 and also nothing happens, and the worst of all is... when I zoom it shows correctly, but if I zoom out then zoom in other part of the chart, most of the months don't show, not because of lack of space, not sure why?, please take a look:
and when zooming out... (at the beginning it was showing just some months because of space, how did this get the space now?)
and when zooming back in...
please take a look at my code below:
xAxis: [
{
tickInterval: 1000 * 60 * 60 * 24 * 30,
labels: {
allowOverlap: true,
format: '{value:%b}',
style: {
fontSize: '8px'
},
rotate: 90,
},
units: [
[
'month',
[1]
]
]
},
{
currentDateIndicator: {
width: 1,
dashStyle: 'dash',
color: 'red',
label: undefined,
},
tickInterval: 1000 * 60 * 60 * 24 * 30,
units: [[
'month',
[3]
],],
labels: {
align: "center",
allowOverlap: true,
formatter: function AxisLabelsFormatterCallbackFunction() {
var providedLocalDate = new Date(this.value);
var realDate = new Date(providedLocalDate.getTime() + (providedLocalDate.getTimezoneOffset() * 60 * 1000));
var month = realDate.getMonth();
var QuarterNumber;
var MonthInits = [];
if (month >= 0 && month <= 2) {
QuarterNumber = 1;
MonthInits[0] = 'J';
MonthInits[1] = 'F';
MonthInits[2] = 'M';
}
if (month >= 3 && month <= 5) {
QuarterNumber = 2;
MonthInits[0] = 'A';
MonthInits[1] = 'M';
MonthInits[2] = 'J';
}
if (month >= 6 && month <= 8) {
QuarterNumber = 3;
MonthInits[0] = 'J';
MonthInits[1] = 'A';
MonthInits[2] = 'S';
}
if (month >= 9 && month <= 11) {
QuarterNumber = 4;
MonthInits[0] = 'O';
MonthInits[1] = 'N';
MonthInits[2] = 'D';
}
return `<div style="font-size: x-small; font-family:arial;font-weight:bold; color:${((QuarterNumber == 1 | QuarterNumber == 3) ? "#B86B00;" : "#0068B5;")};">QTR ${QuarterNumber}</div>`;
},
},
},
{
tickInterval: 1000 * 60 * 60 * 24 * 365, // Year
labels: {
format: '{value:%Y}',
style: {
fontSize: '15px'
}
},
},
],

Stacked, grouped column chart with variable width of x points

I'm trying to dynamically set the width of each x point based on the number of non-zero stacks with that x value.
Example:
What I have:
What I want:
I want to get rid of the empty spaces by lowering the width of each date. (Sep 7 should have width 2, Sep 8 and Sep 9 should have width 1, instead of all 3 dates having width 3)
Here's a more extreme example, lots of wasted space:
I've looked into variwide charts but I can't seem to find an example of a variwide chart with both stacking and grouping.
The closest question I was able to find on stackoverflow is Display Different Number of Groups in Highcharts Stacked Column Graph but that answer doesn't change the width of the points, it just centers the bars.
JSFiddle: http://jsfiddle.net/t3z76o4b/3/
$(function() {
$('#container').highcharts({
chart: {
type: 'column',
},
legend: {
enabled: false
},
title: {
text: 'Fruits by day'
},
xAxis: {
type: 'category',
labels: {
rotation: -45,
formatter: function() {
var placeholder = Number(this.value);
if (!!placeholder) {
return ""
}
//var obj = data[this.value];
if (this.axis.series[0].levelNumber == 1 && !this.isFirst) {
return '';
} else {
return this.value;
}
}
},
crosshair: true,
},
plotOptions: {
series: {
stacking: 'normal'
}
},
series: [{
name: "Apples",
stack: "Apples",
date: "7 Sep 2018",
data: [{
color: "rgba(51,193,59,1)",
name: "7 Sep 2018",
y: 1
},
{
color: "rgba(51,193,50,0.4)",
name: "7 Sep 2018",
y: 2
}],
},
{
name: "Blueberries",
stack: "Blueberries",
date: "7 Sep 2018",
data: [{
color: "rgba(51,50,250,1)",
name: "7 Sep 2018",
y: 3
},
{
color: "rgba(51,50,250,0.4)",
name: "7 Sep 2018",
y: 1
}],
},
{
name: "Oranges",
stack: "Oranges",
date: "8 Sep 2018",
data: [{
color: "rgba(250,193,10,0.5)",
name: "8 Sep 2018",
y: 1
}, ],
},
{
name: "Blueberries",
stack: "Blueberries",
date: "9 Sep 2018",
data: [{
color: "rgba(51,50,250,1)",
name: "9 Sep 2018",
y: 2
}],
},
]
});
});
In the above JSFiddle, each series element represents a certain fruit on a certain day:
series[ 0] = Apples on Sep 7 2018
series[ 1] = Blueberries on Sep 7 2018
series[ 2] = Oranges on Sep 8 2018
series[ 3] = Blueberries on Sep
9 2018
Thanks in advance!
Unfortunately, Highcharts calculate groups space as xAxis width divided by categories length. So this space will be always equal among categories. The chart as you showed above requires a lot of changes in the core Highcharts functions and it is tricky to achieve.
Only centring columns in the group can be done with a bit of custom code:
var stacks = [
'section 1',
'section 2',
'section 3',
'section 4',
'section 5',
'section 6',
'section 7'
];
var categoriesStacksColl = [];
var seriesStackColl = {};
function calculateColumnTranslate(params) {
var m = params.stackLen / 2 + 0.5, // middle bar + offset to calc
barIndex = params.stackIndex,
a = Math.abs(barIndex - m), // bar offset from the middle point
barW = params.barW,
p = params.padding,
posX,
translateX;
if (barIndex === m) {
posX = -barW / 2;
} else if (barIndex > m) {
posX = a * barW + a * p - barW / 2;
} else {
posX = -a * barW - a * p - barW / 2;
}
translateX = posX - params.offset;
return translateX;
}
// Inside Highcharts options
chart: {
type: 'column',
events: {
load: function() {
var chart = this,
series = chart.series,
categoriesLen = chart.xAxis[0].tickPositions.length,
changeWidthFlag = false,
seriesPoints,
nextSeriesPoints,
stack,
length,
arrIndex,
i,
j;
categoriesStacksColl = [];
// Init stacks per categories array
for (j = 0; j < categoriesLen; j++) {
categoriesStacksColl.push(stacks.slice());
}
series.forEach(function(singleSeries) {
stack = singleSeries.options.stack;
if (!seriesStackColl[stack]) {
seriesStackColl[stack] = [];
}
seriesStackColl[stack].push(singleSeries);
});
stacks.forEach(function(initStack) {
seriesPoints = seriesStackColl[initStack][0].points;
length = seriesStackColl[initStack].length;
seriesPoints.forEach(function(point, index) {
if (!point.y && length === 1) {
// increase column width
changeWidthFlag = true;
} else if (!point.y && length > 1) {
changeWidthFlag = true;
for (i = 1; i < length; i++) {
nextSeriesPoints = seriesStackColl[initStack][i].points;
if (nextSeriesPoints[index].y) {
changeWidthFlag = false;
}
}
}
// when all points in category stack are null
if (changeWidthFlag) {
arrIndex = categoriesStacksColl[index].indexOf(initStack);
categoriesStacksColl[index].splice(arrIndex, 1);
changeWidthFlag = false;
}
});
});
},
render: function() {
var chart = this,
series = chart.series[0],
columnMetrics = series.columnMetrics,
barW = columnMetrics.width,
barOffsets = {},
offsets = [],
columnsToTranslate = [],
offsetMin = 0,
offsetMax = 0,
columnsGroupLen = stacks.length,
offset,
columnsGroupWidth,
padding,
point,
pointOffset,
stackIndex,
stackLen,
pointOffsetTemp,
translateBarX;
stacks.forEach(function(stack) {
if (seriesStackColl[stack][0].visible) {
offset = seriesStackColl[stack][0].columnMetrics.offset;
offsetMax = offsetMax < offset ? offset : offsetMax;
offsetMin = offsetMin > offset ? offset : offsetMin;
barOffsets[stack] = offset;
offsets.push(offset);
}
});
columnsGroupWidth = Math.abs(offsetMin) + Math.abs(offsetMax) + barW;
padding = (columnsGroupWidth - columnsGroupLen * barW) / (columnsGroupLen - 1);
categoriesStacksColl.forEach(function(cat, index) {
if (cat.length < stacks.length) {
columnsToTranslate.push({
index: index,
stack: cat
});
}
});
columnsToTranslate.forEach(function(elem) {
stackIndex = 0;
pointOffsetTemp = 0;
chart.series.forEach(function(singleSeries) {
point = singleSeries.points[elem.index];
if (point.y && singleSeries.visible) {
pointOffset = point.series.columnMetrics.offset;
stackLen = elem.stack.length;
if (pointOffsetTemp !== pointOffset) {
pointOffsetTemp = pointOffset;
stackIndex++;
}
translateBarX = calculateColumnTranslate({
padding: padding,
barW: barW,
offset: pointOffset,
stackIndex: stackIndex,
stackLen: stackLen
});
point.graphic.translate(translateBarX);
}
});
});
}
}
}
Demo:
https://jsfiddle.net/wchmiel/9eb74hys/2/

highcharts, Set minimum height for stacked column chart?

I have a 3D stacked column chart.
If there is some larger values in the data, the small values will not be shown in the chart.
As you can see in
http://jsfiddle.net/43pv1a2q/6/
series: [{
name: 'John',
data: [500, 3, 4, 7, 2], //If change 500 to 5, all blocks will be shown
stack: 'male'
}, {
name: 'Joe',
data: [300, 4, 4, 2, 5], //change 300 to 3
stack: 'male'
},
{
name: 'Tom',
data: [500, 3, 4, 7, 2], // change 500 to 5
stack: 'male'
}]
The minPointLength works with bar chart, but not with stacked column chart.
https://api.highcharts.com/highcharts/series.columnrange.minPointLength
How do you set a minimum height for the block in a stacked column?
It seems to be a bug. You can report it here: https://github.com/highcharts/highcharts/issues
Workaround:
I update every point using a new value if its original y value is less than 50 (threshold) and save the original value in realValue property. Then I manually compute the cumulative values for every stack in tooltip.pointFormatter so that the viewer sees proper values:
events: {
load: function() {
var chart = this,
minColHeightVal = 50;
chart.series.forEach(function(s) {
s.points.forEach(function(p) {
if (p.y < minColHeightVal) {
p.update({
y: minColHeightVal,
realValue: p.y
}, false);
}
});
});
chart.redraw();
}
}
// (...)
pointFormatter: function() {
var stackSum = 0,
point = this,
chart = point.series.chart;
chart.series.forEach(function(s) {
s.points.forEach(function(p) {
if (p.x === point.x) {
stackSum += p.realValue ? p.realValue : p.y
}
});
});
return '<span style="color:' + this.color + '">\u25CF</span> ' + this.series.name + ': ' + (point.realValue ? point.realValue : point.y) + ' / ' + stackSum;
}
Live demo: http://jsfiddle.net/kkulig/j3toufk9/
The solution might be to set the y-axis to type: logarithmic, like so: http://jsfiddle.net/43pv1a2q/8/
yAxis: {
type: 'logarithmic',
allowDecimals: false,
title: {
text: 'Number of fruits',
skew3d: true
}
},
The only change I've made is to set "type: 'logarithmic' and removed "min: 0". I can't think of any other way to achieve what you're looking for when working with such hugely different numbers.
EDIT: Of course, you can still use "min: X" to set minimum value on the y-axis; I just removed it because it was unnecessary when I wanted minimum to default.

How do I prevent Highstock right range handle keep slipping off the max value when adding data

I have a Highstock chart with three lines. When I add data, it will at in semi regular cases let the max range handle move off the right side, and then stay put instead of staying glued to the right side as expected. This behavior is not wanted.
Before it slips:
After it slips:
I have made a simplified example
https://jsfiddle.net/eskil_saatvedt/rdwdbht1/3/
HTML
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<div id="container" style="height: 400px; min-width: 310px"></div>
Javascript
$(function() {
Highcharts.setOptions({
global: {
useUTC: false
}
});
// Create the chart
$('#container').highcharts('StockChart', {
chart: {
type: 'line',
},
rangeSelector: {
buttons: [{
count: 1,
type: 'minute',
text: '1M'
}, {
count: 5,
type: 'minute',
text: '5M'
}, {
type: 'all',
text: 'All'
}],
inputEnabled: false,
selected: 2
},
title: {
text: 'Live random data'
},
exporting: {
enabled: false
},
series: [{
name: 'Random data',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime();
data.push([
time +1 * 1000,
Math.round(Math.random() * 100)
]);
return data;
}())
},
{
name: 'Random data2',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime();
data.push([
time +1 * 1000,
Math.round(Math.random() * 100)
]);
return data;
}())
},
{
name: 'Random data3',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime();
data.push([
time +1 * 1000,
Math.round(Math.random() * 100)
]);
return data;
}())
}
]
});
});
function UpdateData() {
var chart = $('#container').highcharts();
var x = (new Date()).getTime(), // current time
y = Math.round(Math.random() * 100),
k = Math.round(Math.random() * 100)+50,
z = Math.round(Math.random() * 100)+20;
chart.series[0].addPoint([x, y], false, false);
chart.series[1].addPoint([x, z], false, false);
chart.series[2].addPoint([x, k], false, false);
chart.redraw();
}
setInterval(function() {
UpdateData();
}, 1000);
Normally I would display 2 temperatures and gain, using it for room heat control.
You need to call setExtremes()](http://api.highcharts.com/highcharts#Axis.setExtremes) function after adding point with new range.
function UpdateData() {
var chart = $('#container').highcharts(),
min = chart.xAxis[0].dataMin,
max = chart.xAxis[0].dataMax,
x = (new Date()).getTime(), // current time
y = Math.round(Math.random() * 100),
k = Math.round(Math.random() * 100) + 50,
z = Math.round(Math.random() * 100) + 20;
chart.series[0].addPoint([x, y], false, false);
chart.series[1].addPoint([x, z], false, false);
chart.series[2].addPoint([x, k], false, false);
chart.xAxis[0].setExtremes(min,x);
}
Example:
- https://jsfiddle.net/xh1sjk3L/
Thanks to Sebastian.
By using max = chart.xAxis[0].userMax I was able to set the max value to max if it is close to the side. In the example 1 second
function UpdateData() {
var chart = $('#container').highcharts();
var userMax = chart.xAxis[0].userMax; // <- change
var x = (new Date()).getTime(), // current time
y = Math.round(Math.random() * 100),
k = Math.round(Math.random() * 100)+50,
z = Math.round(Math.random() * 100)+20;
chart.series[0].addPoint([x, y], false, false);
chart.series[1].addPoint([x, z], false, false);
chart.series[2].addPoint([x, k], false, false);
chart.redraw(); // Redraw it again, as the left handle position I want is most often correct after the redraw.
// ----- change --------
var deltaX = x - userMax;
if (deltaX < 1000){ // the handle was closer to the end than 1 second
chart.xAxis[0].setExtremes(chart.xAxis[0].min,x);
}
}

Highcharts donut label overflow container

I am attempting to create a donut chart with (long) labels. The container for the chart is small (and dynamic). I keep running into a problem where the labels for the outer chart are cutoff.
series: [{
name: 'Browsers',
data: browserData,
size: '65%',
dataLabels: {
formatter: function() {
return this.y > 5 ? this.point.name : null;
},
color: 'white',
distance: -30
}
}, {
name: 'Versions',
data: versionsData,
size: '85%',
innerSize: '65%',
dataLabels: {
formatter: function() {
// display only if larger than 1
return this.y > 1 ? '<b>'+ this.point.name +':</b> '+ this.y +'%' : null;
}
}
}]
jsfiddle: http://jsfiddle.net/sw99B/
What I am trying to accomplish is the auto pie size, as is the case with simple pies:
series: [{
name: 'Versions',
data: versionsData,
dataLabels: {
formatter: function() {
// display only if larger than 1
return this.y > 1 ? '<b>'+ this.point.name +':</b> '+ this.y +'%' : null;
}
}
}]
jsfiddle: http://jsfiddle.net/4P4D5/
The problem with donuts is that I have to specify the size of the inner & outer donut. This causes the two pies making the donut to have fixed radius. Ideally, I would have the inner pie be a percentage of the outer; and let the outer pie have an auto-size.
Any suggestions on how to accomplish this?
The dataLabels: formatter... is only controlling whether or not to show datalabel for thinner slices. It's not controlling the size of the pie chart. That is being specified by the series:[{size parameter. In your example it's set to 85% of the container which does lead to overflow.
According to the docs
The default behaviour (as of 3.0) is to scale to the plot area and
give room for data labels within the plot area.
If you comment out the size on your example, it does indeed squeeze in the labels but I'm not sure you'll love the look of it.
The following minor patch to highcharts.src.js v4.0.1 achieves the desired behaviour:
--- highcharts.src.orig.js 2014-04-24 08:25:52.000000000 +0000
+++ highcharts.src.js 2014-06-24 13:57:42.957605307 +0000
## -12167,6 +12167,22 ##
positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0],
smallestSize = mathMin(plotWidth, plotHeight),
isPercent;
+
+ /**
+ * Allow a chart (pie) to specify a size relative to another series. In
+ * that case, simply copy the center position of the parent, and scale
+ * the radius.
+ */
+ if ( options.relativeSize ) {
+ parentPositions = chart.series[options.relativeSize.parentSeries].center;
+ positions[0] = parentPositions[0];
+ positions[1] = parentPositions[1];
+ positions[2] = options.relativeSize.size * parentPositions[2];
+
+ return map(positions, function (length, i) {
+ return positions[i]
+ });
+ }
return map(positions, function (length, i) {
isPercent = /%$/.test(length);
Then, when configuring the donut:
series: [{
name: 'Browsers',
data: browserData,
center: ['50%', '50%'],
dataLabels: {
formatter: function() {
return this.y > 5 ? this.point.name : null;
},
color: 'white',
distance: -30
}
}, {
name: 'Versions',
data: versionsData,
relativeSize: {
parentSeries: 0, // index of parent series
size: 0.75 // 75% of parent pie
}
dataLabels: {
formatter: function() {
// display only if larger than 1
return this.y > 1 ? ''+ this.point.name +': '+ this.y +'%' : null;
}
}
}]
Note that the parent (outer) pie must be explicitly centered in the container, otherwise in certain cases the two pies may not be concentric.
Also, requiring the index of the series to be specified is clearly not ideal, but in a tightly controlled graph (as is a donut), it seems to do the job.
Unfortunately this option is not available, so I advice you to post your request on the uservoice

Resources