This question is a possible duplicate of
Show N/A in datalabels, when value is null - Highcharts
and
dataLabels for bar chart in Highcharts not displaying for null values in 3.0.8
but workarounds suggested have stopped working in version 5.0.7 of highcharts.
formatter: function () {
if (this.y == null) {
var chart = this.series.chart,
categoryWidth = chart.plotWidth / chart.xAxis[0].categories.length,
offset = (this.point.x) * categoryWidth + categoryWidth / 2,
text = chart.renderer.text('N/A', -999, -999).add();
text.attr({
x: chart.plotLeft + offset - text.getBBox().width / 2, //center label
y: chart.plotTop + chart.plotHeight - 8 // padding
});
} else {
return this.y;
}
}
The issue seems to be still open on github:
https://github.com/highcharts/highcharts/issues/2899
http://jsfiddle.net/90amxpc1/4/
Is there a possible workaround to show something like "N/A" using formatter function or chart.renderer method for null values in column charts?
In new version of Highcharts formatter is not called for null points anymore.
A hacky way to make your code works as it worked before is to wrap drawDataLabels() method and temporary assign some value to null points, e.g. 0, and in formatter check if point.isNull is true.
var H = Highcharts;
H.wrap(H.Series.prototype, 'drawDataLabels', function (p) {
this.points.forEach(point => {
if (point.y === null) {
point.y = 0;
}
});
p.call(this);
this.points.forEach(point => {
if (point.isNull) {
point.y = null;
}
});
});
In data labels config:
dataLabels: {
formatter: function () {
if (this.point.isNull) {
var chart = this.series.chart,
categoryWidth = chart.plotWidth / chart.xAxis[0].categories.length,
offset = (this.point.x) * categoryWidth + categoryWidth / 2,
text = chart.renderer.text('N/A', -999, -999).add();
text.attr({
x: chart.plotLeft + offset - text.getBBox().width / 2, //center label
y: chart.plotTop + chart.plotHeight - 8 // padding
});
return false;
} else {
return this.y;
}
},
example: http://jsfiddle.net/xnxpwt67/
It works, but it is not an elegant and efficient solution. Much better would be drawing labels for nulls, e.g. on load event.
chart: {
type: 'column',
events: {
load: function() {
const categoryWidth = this.plotWidth / this.xAxis[0].categories.length;
this.series[0].points.forEach(point => {
if (point.y === null) {
const offset = point.x * categoryWidth + categoryWidth / 2;
const text = this.renderer.text('N/A', -999, -999).add();
text.attr({
x: this.plotLeft + offset - text.getBBox().width / 2,
y: this.plotTop + this.plotHeight - 8
});
}
})
}
}
},
example: http://jsfiddle.net/xnxpwt67/1/
Related
I have min and max value for x-axis. If my minvalue=0 and maxvalue=23 i want my x-axis scale to range from 0-23 but due to the automatic tickinterval adjustment(eg. 2) my x-axis scale ranges from 0-24. how do i restrict it to 23 ?
You can use tickPositions or tickPositioner option, for example:
const breaks = 10;
const dataMin = 0;
const dataMax = 23;
const step = Math.round((dataMax - dataMin) / breaks * 100) / 100;
Highcharts.chart('container', {
...,
xAxis: {
tickPositioner: function() {
const positions = [];
for (var i = dataMin; i < (dataMax - step / 2); i += step) {
positions.push(Math.round(i * 100) / 100);
}
positions.push(dataMax);
return positions;
}
}
});
Live demo: http://jsfiddle.net/BlackLabel/6m4e8x0y/4927/
API Reference:
https://api.highcharts.com/highcharts/xAxis.tickPositions
https://api.highcharts.com/highcharts/xAxis.tickPositioner
I am using a modified version of the Highcharts Round Corners Plugin thanks to #davcs86. And on the third level of a drilldown there is a bug. I was hoping someone could assist.
Issue: the third level in drill down errors out.
Goal: Make it work.
Bug Demo: http://jsfiddle.net/32a7L41b/ Click on Alaska and then Wave 1 and you will see the bug. Obviously the data is not real.
//Modified Highcharts Round Corners plugin
(function (H) {
var curPercentage = [];
H.wrap(H.seriesTypes.column.prototype, 'translate', function (proceed) {
var options = this.options,
rTopLeft = options.borderRadiusTopLeft || 0,
rTopRight = options.borderRadiusTopRight || 0,
rBottomRight = options.borderRadiusBottomRight || 0,
rBottomLeft = options.borderRadiusBottomLeft || 0,
topMargin = options.topMargin || 0,
bottomMargin = options.bottomMargin || 0;
proceed.call(this);
if (rTopLeft || rTopRight || rBottomRight || rBottomLeft) {
H.each(this.points, function (point) {
var iBottomRight = rBottomRight,
iBottomLeft = rBottomLeft,
iTopRight = rTopRight,
iTopLeft = rTopLeft;
//console.log(point);
if (typeof (curPercentage[point.index]) == 'undefined') {
curPercentage[point.index] = 0;
}
var prevPercentage = curPercentage[point.index];
curPercentage[point.index] += 1.0 * parseFloat(point.percentage).toFixed(6);
//console.log(prevPercentage);
//console.log(curPercentage);
if (prevPercentage == 0 & curPercentage[point.index] == 100) {
// special case, only one value > 0, preserve all border radius
// reset for the next call
curPercentage[point.index] = 0;
} else if (prevPercentage == 0) {
//right side
iBottomRight = 0;
iBottomLeft = 0;
} else if (curPercentage[point.index] == 100) {
//left side
iTopRight = 0;
iTopLeft = 0;
// reset for the next call
curPercentage[point.index] = 0;
} else {
// no radius
iBottomRight = 0;
iBottomLeft = 0;
iTopRight = 0;
iTopLeft = 0;
}
var shapeArgs = point.shapeArgs,
w = shapeArgs.width,
h = shapeArgs.height,
x = shapeArgs.x,
y = shapeArgs.y;
// Preserve the box for data labels
point.dlBox = point.shapeArgs;
point.shapeType = 'path';
point.shapeArgs = {
d: [
'M', x + iTopLeft, y + topMargin,
// top side
'L', x + w - iTopRight, y + topMargin,
// top right corner
'C', x + w - iTopRight / 2, y, x + w, y + iTopRight / 2, x + w, y + iTopRight,
// right side
'L', x + w, y + h - iBottomRight,
// bottom right corner
'C', x + w, y + h - iBottomRight / 2, x + w - iBottomRight / 2, y + h, x + w - iBottomRight, y + h + bottomMargin,
// bottom side
'L', x + iBottomLeft, y + h + bottomMargin,
// bottom left corner
'C', x + iBottomLeft / 2, y + h, x, y + h - iBottomLeft / 2, x, y + h - iBottomLeft,
// left side
'L', x, y + iTopLeft,
// top left corner
'C', x, y + iTopLeft / 2, x + iTopLeft / 2, y, x + iTopLeft, y,
'Z']
};
});
}
});
}(Highcharts));
Well, I had to modify drilldown.js to support the data saved in point.dlBox
Chart.prototype.addSingleSeriesAsDrilldown = function (point, ddOptions) {
/// (...)
// Add a record of properties for each drilldown level
level = {
levelNumber: levelNumber,
seriesOptions: oldSeries.options,
levelSeriesOptions: levelSeriesOptions,
levelSeries: levelSeries,
shapeArgs: point.dlBox || point.shapeArgs, // <== here
JSFiddle demo
drilldown: {
series: [{
borderRadius: 7, // <----------
id: 'alaskawaves',
name: 'Alaska Waves',
I got it worked by adding borderRadius: 7 inside series object. If you see any drilled down bar as square, just add this borderRadius inside related series object.
Here is example:
http://jsfiddle.net/ht1u0og2/
Take a typical cubic bezier curve drawn in JavaScript (this example I googled...)
http://jsfiddle.net/atsanche/K38kM/
Specifically, these two lines:
context.moveTo(188, 130);
context.bezierCurveTo(170, 10, 350, 10, 388, 170);
We have a cubic bezier which starts at 188, 130, ends at 388, 170, and has controls points a:170, 10 and b:350, 10
My question is would it be possible to mathematically adjust the end point and control points to make another curve which is only a segment of the original curve?
The ideal result would be able to able to take a percentage slice of the bezier from the beginning, where 0.5 would draw only half of the bezier, 0.75 would draw most of the bezier (and so on)
I've already gotten working a few implementations of De Castelau which allow me to trace the contour of the bezier between [0...1], but this doesn't provide a way to mathematically recalculate the end and control points of the bezier to make a sub-bezier...
Thanks in advance
De Casteljau is indeed the algorithm to go. For a cubic Bezier curve defined by 4 control points P0, P1, P2 and P3, the control points of the sub-Bezier curve (0, u) are P0, Q0, R0 and S0 and the control points of the sub-Bezier curve (u, 1) are S0, R1, Q2 and P3, where
Q0 = (1-u)*P0 + u*P1
Q1 = (1-u)*P1 + u*P2
Q2 = (1-u)*P2 + u*P3
R0 = (1-u)*Q0 + u*Q1
R1 = (1-u)*Q1 + u*Q2
S0 = (1-u)*R0 + u*R1
Please note that if you want to "extract" a segment (u1, u2) from the original Bezier curve, you will have to apply De Casteljau twice. The first time will split the input Bezier curve C(t) into C1(t) and C2(t) at parameter u1 and the 2nd time you will have to split the curve C2(t) at an adjusted parameter u2* = (u2-u1)/(1-u1).
This is how to do it. You can get the left half or right half with this functin. This function is take thanks to mark from here: https://stackoverflow.com/a/23452618/1828637
I have it modified so it can be fit to a unit cell so we can use it for cubic-bezier in css transitions.
function splitCubicBezier(options) {
var z = options.z,
cz = z-1,
z2 = z*z,
cz2 = cz*cz,
z3 = z2*z,
cz3 = cz2*cz,
x = options.x,
y = options.y;
var left = [
x[0],
y[0],
z*x[1] - cz*x[0],
z*y[1] - cz*y[0],
z2*x[2] - 2*z*cz*x[1] + cz2*x[0],
z2*y[2] - 2*z*cz*y[1] + cz2*y[0],
z3*x[3] - 3*z2*cz*x[2] + 3*z*cz2*x[1] - cz3*x[0],
z3*y[3] - 3*z2*cz*y[2] + 3*z*cz2*y[1] - cz3*y[0]];
var right = [
z3*x[3] - 3*z2*cz*x[2] + 3*z*cz2*x[1] - cz3*x[0],
z3*y[3] - 3*z2*cz*y[2] + 3*z*cz2*y[1] - cz3*y[0],
z2*x[3] - 2*z*cz*x[2] + cz2*x[1],
z2*y[3] - 2*z*cz*y[2] + cz2*y[1],
z*x[3] - cz*x[2],
z*y[3] - cz*y[2],
x[3],
y[3]];
if (options.fitUnitSquare) {
return {
left: left.map(function(el, i) {
if (i % 2 == 0) {
//return el * (1 / left[6])
var Xmin = left[0];
var Xmax = left[6]; //should be 1
var Sx = 1 / (Xmax - Xmin);
return (el - Xmin) * Sx;
} else {
//return el * (1 / left[7])
var Ymin = left[1];
var Ymax = left[7]; //should be 1
var Sy = 1 / (Ymax - Ymin);
return (el - Ymin) * Sy;
}
}),
right: right.map(function(el, i) {
if (i % 2 == 0) {
//xval
var Xmin = right[0]; //should be 0
var Xmax = right[6];
var Sx = 1 / (Xmax - Xmin);
return (el - Xmin) * Sx;
} else {
//yval
var Ymin = right[1]; //should be 0
var Ymax = right[7];
var Sy = 1 / (Ymax - Ymin);
return (el - Ymin) * Sy;
}
})
}
} else {
return { left: left, right: right};
}
}
Thats the function and now to use it with your parameters.
var myBezier = {
xs: [188, 170, 350, 388],
ys: [130, 10, 10, 170]
};
var splitRes = splitCubicBezier({
z: .5, //percent
x: myBezier.xs,
y: myBezier.ys,
fitUnitSquare: false
});
This gives you
({
left: [188, 130, 179, 70, 219.5, 40, 267, 45],
right: [267, 45, 314.5, 50, 369, 90, 388, 170]
})
fiddle proving its half, i overlaid it over your original:
http://jsfiddle.net/K38kM/8/
Yes it is! Have a look at the bezier section here
http://en.m.wikipedia.org/wiki/De_Casteljau's_algorithm
It is not that difficult all in all.
When drawing a linechart with gRaphael using milliseconds along the x-axis I commonly get inconsistencies in the placement of the data points. Most commonly the initial data points are to the left of the y-axis (as seen in the fiddle below), sometimes the last data-point will be beyond the right side of the view-box/past the termination of the x-axis.
Does anyone know:
1) Why this occurs,
2) How to prevent it, &/or
3) How to check for it (I can use transform to move the lines/points if I know when it has happened/by how much).
my code:
var r = Raphael("holder"),
txtattr = { font: "12px sans-serif" };
var r2 = Raphael("holder2"),
txtattr2 = { font: "12px sans-serif" };
var x = [], y = [], y2 = [], y3 = [];
for (var i = 0; i < 1e6; i++) {
x[i] = i * 10;
y[i] = (y[i - 1] || 0) + (Math.random() * 7) - 3;
}
var demoX = [[1, 2, 3, 4, 5, 6, 7],[3.5, 4.5, 5.5, 6.5, 7, 8]];
var demoY = [[12, 32, 23, 15, 17, 27, 22], [10, 20, 30, 25, 15, 28]];
var xVals = [1288885800000, 1289929440000, 1290094500000, 1290439560000, 1300721700000, 1359499228000, 1359499308000, 1359499372000];
var yVals = [80, 76, 70, 74, 74, 78, 77, 72];
var xVals2 = [1288885800000, 1289929440000];
var yVals2 = [80, 76];
var lines = r.linechart(10, 10, 300, 220, xVals, yVals, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true })
.hoverColumn(function () {
this.tags = r.set();
for (var i = 0, ii = this.y.length; i < ii; i++) {
this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }]));
}
}, function () {
this.tags && this.tags.remove();
});
lines.symbols.attr({ r: 3 });
var lines2 = r2.linechart(10, 10, 300, 220, xVals2, yVals2, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true })
.hoverColumn(function () {
this.tags = r2.set();
for (var i = 0, ii = this.y.length; i < ii; i++) {
this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }]));
}
}, function () {
this.tags && this.tags.remove();
});
lines2.symbols.attr({ r: 3 });
I do have to use gRaphael and the x-axis has to be in milliseconds (it is labeled later w/customized date strings)
Primary example fiddle: http://jsfiddle.net/kcar/aNJxf/
Secondary example fiddle (4th example on page frequently shows both axis errors):
http://jsfiddle.net/kcar/saBnT/
root cause is the snapEnds function (line 718 g.raphael.js), the rounding it does, while fine in some cases, is adding or subtracting years from/to the date in other cases.
Haven't stepped all the way through after this point, but since the datapoints are misplaced every time the rounding gets crazy and not when it doesn't, I'm going to go ahead and assume this is causing issues with calculating the chart columns, also before being sent to snapEnds the values are spot on just to confirm its not just receiving miscalculated data.
code of that function from g.raphael.js
snapEnds: function(from, to, steps) {
var f = from,
t = to;
if (f == t) {
return {from: f, to: t, power: 0};
}
function round(a) {
return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a);
}
var d = (t - f) / steps,
r = ~~(d),
R = r,
i = 0;
if (r) {
while (R) {
i--;
R = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
}
i ++;
} else {
if(d == 0 || !isFinite(d)) {
i = 1;
} else {
while (!r) {
i = i || 1;
r = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
i++;
}
}
i && i--;
}
t = round(to * Math.pow(10, i)) / Math.pow(10, i);
if (t < to) {
t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
}
f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
return { from: f, to: t, power: i };
},
removed the rounding nonsense from snapEnds and no more issues, not noticed any downside from either axis or any other area of the chart. If you see one I'd love to hear it though.
code of that function from g.raphael.js now:
snapEnds: function(from, to, steps) {
return {from: from, to: to, power: 0};
},
Hi if you comment this:
if (valuesy[i].length > width - 2 * gutter) {
valuesy[i] = shrink(valuesy[i], width - 2 * gutter);
len = width - 2 * gutter;
}
if (valuesx[i] && valuesx[i].length > width - 2 * gutter) {
valuesx[i] = shrink(valuesx[i], width - 2 * gutter);
}
in g.line.js, It seems to solve the problem, and it also solves a similar problem with the values in the y axis.
Upgrading from v0.50 to v0.51 fixed the issue for me.
Still not sure why it occurs, adding in a transparent set was not a desirable option.
The simplest way to check for if the datapoints were rendered outside of the graph seems to be getting a bounding box for the axis set and a bounding box for the datapoints and checking the difference between the x and x2 values.
If anyone can help me with scaling the datapoint set, or figure out how to make this not happen at all, I will still happily appreciate/up vote answers
//assuming datapoints is the Raphael Set for the datapoints, axes is the
//Raphael Set for the axis, and datalines is the Raphael Set for the
//datapoint lines
var pointsBBox = datapoints.getBBox();
var axesBBox = axes.getBBox();
var xGapLeft = Math.ceil(axesBBox.x - pointsBBox.x);
//rounding up to integer to simplify, and the extra boost from y-axis doesn't
//hurt, <1 is a negligible distance in transform
var xGapRight = Math.ceil(axesBBox.x2 - pointsBBox.x2);
var xGap = 0;
if(xGapLeft > 0){
datapoints.transform('t' +xGapLeft +',0');
datalines.transform('t' +xGapLeft +',0');
xGap = xGapLeft;
}else if (xGapRight < 0) { //using else if because if it is a scale issue it will
//be too far right & too far left, meaning both are true and using transform will
//just shift it right then left and you are worse off than before, using
//set.transform(scale) works great on dataline but when using on datapoints scales
// symbol radius not placement
if (xGapLeft < 0 && xGapRight < xGapLeft) { xGapRight = xGapLeft; }
//in this case the initial point is right of y-axis, the end point is right of
//x-axis termination, and the difference between last point/axis is greater than
//difference between first point/axis
datapoints.transform('t' +xGapRight +',0');
datalines.transform('t' +xGapRight +',0');
xGap = xGapRight;
}
rehookHoverOverEvent(xGap); //there are so many ways to do this just leaving it
//here as a call to do so, if you don't the hover tags will be over the original
//datapoints instead of the new location, at least they were in my case.
im stuck resolving an issue.
i want to build a bar chart using raphaeljs or another, the main funccionality is that once drawn, the bars should be resizable by the mouse dragging and be able to get their current value once the drag stopped.
i have tried the bellow code but its not working.
function drawchart()
{
var data = [1,2,3];
/*
* Create an instance of raphael and specify:
* the ID of the div where to insert the graph
* the width
* the height
* Tip: Remember that the reference point (0, 0) is at the top left position.
*/
var r = Raphael("holder",600,300);
// start, move, and up are the drag functions
start = function () {
// storing original coordinates
this.ox = this.attr("x");
this.oy = this.attr("y");
this.attr({opacity: 1});
this.sizer.ox = this.sizer.attr("x");
this.sizer.oy = this.sizer.attr("y");
this.sizer.attr({opacity: 1});
},
move = function (dx, dy) {
// move will be called with dx and dy
this.attr({x: this.ox + dx, y: this.oy + dy});
this.sizer.attr({x: this.sizer.ox + dx, y: this.sizer.oy + dy});
},
up = function () {
// restoring state
this.attr({opacity: .5});
this.sizer.attr({opacity: .5});
},
rstart = function () {
// storing original coordinates
this.ox = this.attr("x");
this.oy = this.attr("y");
this.box.ow = this.box.attr("width");
this.box.oh = this.box.attr("height");
},
rmove = function (dx, dy) {
// move will be called with dx and dy
this.attr({x: this.ox + dx, y: this.oy + dy});
this.box.attr({width: this.box.ow + dx, height: this.box.oh + dy});
};
var chart = r.g.barchart(5, 5, 200, 280, data, {stacked: false, type: "square"}).label(['a','b','c']);
chart.drag(move, start, up);
chart.hover(function() {
// Create a popup element on top of the bar
this.flag = r.g.popup(this.bar.x, this.bar.y, (this.bar.value || "0") + "%").insertBefore(this);
}, function() {
// hide the popup element with an animation and remove the popup element at the end
this.flag.animate({opacity: 0}, 300, function () {this.remove();});
});
/*
* Define the default text attributes before writing the labels
*/
r.g.txtattr = {font:"12px Fontin-Sans, Arial, sans-serif", fill:"#000", "font-weight": "bold"};
// iterate over all the bar
for (var i = 0; i < chart.bars[0].length; i++) {
var bar = chart.bars[0][i];
// if the value of the bar is greater or equals to 15 we change the color to red
if (bar.value >= 15) {
bar.attr("fill", "#bf2f2f");
bar.attr("stroke", "#bf2f2f");
}
}
}
any kind of help will be appreciated.
thanks in advance