Highcharts - dynamic data label positioning on multi series doughtnut charts - javascript

Background info:
I am trying to create a multi series doughnut chart with the data labels positioned in the center of each slice like this (see below) however I want the data labels to reposition itself if the div is resized:
Problem:
I am struggling to reposition the data labels accurately when the div is resized.
I need to get the current doughnut chart plot size in percentage with which I could calculate accurate distance from doughnut edges to place the data labels.
Example Fiddle
Here's a mockup in fiddle: http://jsfiddle.net/simkus/bo2eumkd/30/
$(function() {
var container = $('#container')[0];
$('#resizer').resizable({
// On resize, set the chart size to that of the
// resizer minus padding. If your chart has a lot of data or other
// content, the redrawing might be slow. In that case, we recommend
// that you use the 'stop' event instead of 'resize'.
// TODO we need to calculate the distance of dataLabels from the edges of the
// doughnut chart. The main thing is that the chart might be resized, to e.g. 200pxx200px
// and then the labels now gets scatared by the current algorythm where we calculate `distance`
// the `distance` should be calculated I think not in pixels by % of the current
// chart placeholder size. The chart it self has a `size` of 80% for all series.
// the inner size is also measured in %, so the distance somehow has to be done
// similary, but at this point, the `distance` doesn't work with % as size and innerSize
resize: function() {
chart.setSize(
this.offsetWidth - 20,
this.offsetHeight - 20,
false
);
}
});
let seriesData = [{
"base": 1949,
"data": [106, 105, 26, 43, 55, 136],
"name": "Total",
},
{
"base": 871,
"data": [38, 44, 14, 20, 25, 75],
"name": "Male",
},
{
"base": 1063,
"data": [65, 61, 12, 23, 31, 60],
"name": "Female",
}
]
let categories = [
"Don't know",
"Australia",
"India",
"Japan",
"Brazil",
"I don’t know"
]
let series = [];
let plotArea = 350;
let centerInnerSize = 40; // 40%
for (let i = 0; i < seriesData.length; i++) {
var showInLegend = false,
data = [],
innerSize = centerInnerSize + ((100 - centerInnerSize) / seriesData.length) * i,
width = (plotArea / 2) * ((1 - centerInnerSize / 100) / seriesData.length),
distance = (plotArea / 2) * (1 - innerSize / 100) - width / 2;
if (i == 0) {
showInLegend = true;
}
for (let j = 0; j < categories.length; j++) {
data.push({
'name': categories[j],
'y': seriesData[i].data[j]
})
}
series.push({
name: seriesData[i].name,
data: data,
innerSize: innerSize + '%',
size: "80%",
showInLegend: true,
dataLabels: {
enabled: true,
distance: -distance,
}
})
}
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
plotBackgroundColor: null,
plotBorderWidth: 1,
plotShadow: false,
type: 'pie',
events: {
render() {
let chart = this;
chart.series.forEach(s => {
s.points.forEach(p => {
let label = p.dataLabel;
p.dataLabel.translate(label.translateX, label.translateY)
})
})
}
}
},
subtitle: {
text: 'Drag the handle in the lower right to resize'
},
tooltip: {
pointFormat: '{series.name}: <b>{point.percentage:.2f}%</b>'
},
plotOptions: {
pie: {
allowPointSelect: false,
cursor: 'pointer',
minSize: 1,
dataLabels: {
enabled: true,
color: '#000000',
format: '{percentage:.2f} %',
}
}
},
series: series,
});
});
Any ideas how I can resolve this problem?

I need to get the current doughnut chart plot size in pixels with which I could calculate accurate distance from doughnut edges to place the data labels.
I am not sure what is your idea to place the data labels, but you should be able to achieve it by using the render callback where you can get access to each point and his label.
Demo: http://jsfiddle.net/BlackLabel/5m41p986/
events: {
render() {
let chart = this;
chart.series.forEach(s => {
s.points.forEach(p => {
let label = p.dataLabel;
p.dataLabel.translate(label.translateX, label.translateY)
})
})
}
}
API: https://api.highcharts.com/highcharts/chart.events.render

Updated the distance calculation using percentage and now it works.
http://jsfiddle.net/4g1pxvqb/36/
for (let i = 0; i < seriesData.length; i++) {
var showInLegend = false,
data = [],
innerSize = centerInnerSize + ((100 - centerInnerSize) / seriesData.length) * i,
width = (100 - centerInnerSize) / 3;
distance = '-' + (10 + width * (2 - i)) + '%';

Related

How can I evenly distribute ticks when using maxTicksLimit?

I made a line chart using Chart.js version 2.1.3.
var canvas = $('#gold_chart').get(0);
var ctx = canvas.getContext('2d');
var fillPatternGold = ctx.createLinearGradient(0, 0, 0, canvas.height);
fillPatternGold.addColorStop(0, '#fdca55');
fillPatternGold.addColorStop(1, '#ffffff');
var goldChart = new Chart(ctx, {
type: 'line',
animation: false,
data: {
labels: dates,
datasets: [{
label: '',
data: prices,
pointRadius: 0,
borderWidth: 1,
borderColor: '#a97f35',
backgroundColor: fillPatternGold
}]
},
title: {
position: 'bottom',
text: '\u7F8E\u5143 / \u76CE\u53F8'
},
options: {
legend: {
display: false
},
tooltips: {
callback: function(tooltipItem) {
return tooltipItem.yLabel;
}
},
scales: {
xAxes: [{
ticks: {
maxTicksLimit: 8
}
}]
}
}
});
The output is as follow:
As you can see, I limited the maximum count of ticks to 8 via maxTicksLimit. However, the distribution is not even. How can I make the ticks distribute evenly?
p.s. there are always 289 records in the dataset, and the data is recorded every 5 minutes. Sample values of prices variable are:
[
{"14:10", 1280.3},
{"14:15", 1280.25},
{"14:20", 1282.85}
]
I tried different values of maxTicksLimit, and the results are still not distributed evenly.
Chart.js uses an integral skipRatio (to figure out how many labels to skip). With Chart.js v2.1.x, you can write your own plugin to use a fractional skipRatio
Preview
Script
Chart.pluginService.register({
afterUpdate: function (chart) {
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
// store the original maxTicksLimit
xScale.options.ticks._maxTicksLimit = xScale.options.ticks.maxTicksLimit;
// let chart.js draw the first and last label
xScale.options.ticks.maxTicksLimit = (xScale.ticks.length % xScale.options.ticks._maxTicksLimit === 0) ? 1 : 2;
var originalXScaleDraw = xScale.draw
xScale.draw = function () {
originalXScaleDraw.apply(this, arguments);
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
var helpers = Chart.helpers;
var tickFontColor = helpers.getValueOrDefault(xScale.options.ticks.fontColor, Chart.defaults.global.defaultFontColor);
var tickFontSize = helpers.getValueOrDefault(xScale.options.ticks.fontSize, Chart.defaults.global.defaultFontSize);
var tickFontStyle = helpers.getValueOrDefault(xScale.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle);
var tickFontFamily = helpers.getValueOrDefault(xScale.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily);
var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily);
var tl = xScale.options.gridLines.tickMarkLength;
var isRotated = xScale.labelRotation !== 0;
var yTickStart = xScale.top;
var yTickEnd = xScale.top + tl;
var chartArea = chart.chartArea;
// use the saved ticks
var maxTicks = xScale.options.ticks._maxTicksLimit - 1;
var ticksPerVisibleTick = xScale.ticks.length / maxTicks;
// chart.js uses an integral skipRatio - this causes all the fractional ticks to be accounted for between the last 2 labels
// we use a fractional skipRatio
var ticksCovered = 0;
helpers.each(xScale.ticks, function (label, index) {
if (index < ticksCovered)
return;
ticksCovered += ticksPerVisibleTick;
// chart.js has already drawn these 2
if (index === 0 || index === (xScale.ticks.length - 1))
return;
// copy of chart.js code
var xLineValue = this.getPixelForTick(index);
var xLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines);
if (this.options.gridLines.display) {
this.ctx.lineWidth = this.options.gridLines.lineWidth;
this.ctx.strokeStyle = this.options.gridLines.color;
xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
// Draw the label area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.moveTo(xLineValue, yTickStart);
this.ctx.lineTo(xLineValue, yTickEnd);
}
// Draw the chart area
if (this.options.gridLines.drawOnChartArea) {
this.ctx.moveTo(xLineValue, chartArea.top);
this.ctx.lineTo(xLineValue, chartArea.bottom);
}
// Need to stroke in the loop because we are potentially changing line widths & colours
this.ctx.stroke();
}
if (this.options.ticks.display) {
this.ctx.save();
this.ctx.translate(xLabelValue + this.options.ticks.labelOffset, (isRotated) ? this.top + 12 : this.options.position === "top" ? this.bottom - tl : this.top + tl);
this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1);
this.ctx.font = tickLabelFont;
this.ctx.textAlign = (isRotated) ? "right" : "center";
this.ctx.textBaseline = (isRotated) ? "middle" : this.options.position === "top" ? "bottom" : "top";
this.ctx.fillText(label, 0, 0);
this.ctx.restore();
}
}, xScale);
}
};
}
},
});
Fiddle - http://jsfiddle.net/bh63pe1v/
A simpler solution until this is permanently fixed by the Chart JS contributors is to include a decimal in maxTicksLimit.
For example:
maxTicksLimit: 8,
produces a huge gap at the end.
maxTicksLimit: 8.1,
Does not produce a huge gap at the end.
Depending on what you want to set your maxTicksLimit to, you need to play around with different decimals to see which one produces the best result.
Just do this:
yAxes: [{
ticks: {
stepSize: Math.round((Math.max.apply(Math, myListOfyValues) / 10)/5)*5,
beginAtZero: true,
precision: 0
}
}]
10 = the number of ticks
5 = rounds tick values to the nearest 5 - all y values will be incremented evenly
Similar will work for xAxes too.

Flot Thresholds on x-axis

I created a realtime (updates every 10ms) vertical spline chart using Flot. The chart can be seen here on Codepen. I included the Flot multiple threshold plugin, but I would like for the thresholds to use the x-axis values (on the bottom of the vertical chart) and not the y-axis values (left of the chart). The plot would then paint all values outside of the dashed black lines in red.
In the example you can see that the thresholds use the y-axis to color thresholds (in my case, all values below constraintMax, which is 60).
The operative lines of code are where I set up the options (line 79 in the update function):
var options = {
xaxis: {
position: 'bottom',
min: -10,
max: 100
},
yaxis: {
position: 'left',
min: iterator,
max: updatedData.length-1+iterator,
transform: function (v) { return -v; },
inverseTransform: function (v) { return -v; }
}
};
Where I set up the constraints (line 66 in the update function):
var constraintMax = {
threshold: 60,
color: "rgb(255,0,0)",
evaluate : function(y,threshold){ return y < threshold; }
}
var constraintMin = {
threshold: 25,
color: "rgb(255,0,0)",
evaluate : function(y,threshold){ return y < threshold; }
}
And where I actually plot (line 93 in the update function):
$.plot("#"+elementID, [{data: updatedData, constraints: [constraintMin, constraintMax]}, {data: initialMinData, color: "#000000", dashes: { show: true }}, {data: initialMaxData, color: "#000000", dashes: { show: true }}], options);
Does anyone have any ideas on how to paint the plot points that are outside of the dashed lines red? Thank you in advance.
The multiple threshold plugin only support y-value thresholds out of the box. Therefore you have to change it for your plot. I copied the code into a jsfiddle (I don't like codepen) and changed it there.
1) Your constraintMax threshold is wrong for what you want to do, you need return y > threshold.
2) Changes in the multiple threshold plugin:
if (evaluate(currentPoint[1], threshold)) {
v
if (evaluate(currentPoint[0], threshold)) {
and
function _getPointOnThreshold(threshold, prevP, currP) {
var currentX = currP[0];
var currentY = currP[1];
var prevX = prevP[0];
var prevY = prevP[1];
var slope = (threshold - currentX) / (prevX - currentX);
var yOnConstraintLine = slope * (prevY - currentY) + currentY;
return [threshold, yOnConstraintLine];
}
See the fiddle for the working example.

Highchart tooltip show nearest point

I have been trying to make highchart tooltip to show the nearest point incase the x-axis value aren't align in different series.
This is what I got so far
http://jsfiddle.net/Yw8hb/5/
Highcharts.wrap(Highcharts.Tooltip.prototype, 'refresh', function (proceed) {
var args = arguments,
points = args[1],
point = points[0],
chart = point.series.chart;
// Loop over all the series of the chart
Highcharts.each(chart.series, function(series) {
// This one already exist
if (series == point.series) return;
var current,
dist,
distance = Number.MAX_VALUE;
// Loop over all the points
Highcharts.each(series.points, function(p) {
// use the distance in X to determine the closest point
dist = Math.abs(p.x - point.x);
if (dist < distance) {
distance = dist;
current = p;
}
});
// Add the closest point to the array
points.push(current);
});
proceed.apply(this, [].slice.call(args, 1));
});
It seems to be working half way there however when you hover in some spot it shows duplicated series. I have spent hours trying to figure this out any help would be very appreciated.
Before insertion, check whether points array contains the current point in your refresh callback function.
// Add the closest point to the array
if(points.indexOf(current)==-1)
points.push(current);
Highcharts.wrap(Highcharts.Tooltip.prototype, 'refresh', function (proceed) {
var args = arguments,
points = args[1],
point = points[0],
chart = point.series.chart;
// Loop over all the series of the chart
Highcharts.each(chart.series, function(series) {
// This one already exist
if (series == point.series) return;
var current,
dist,
distance = Number.MAX_VALUE;
// Loop over all the points
Highcharts.each(series.points, function(p) {
// use the distance in X to determine the closest point
dist = Math.abs(p.x - point.x);
if (dist < distance) {
distance = dist;
current = p;
}
});
// Add the closest point to the array
if(points.indexOf(current)==-1)
points.push(current);
});
proceed.apply(this, [].slice.call(args, 1));
});
$('#container').highcharts({
tooltip: {
shared: true
},
xAxis: {
crosshair: {
color: '#F70000'
}
},
series: [{
data: [{
x: 0.0,
y: 1
}, {
x: 1.0,
y: 2
}, {
x: 2.0,
y: 3
}, {
x: 3.0,
y: 2
}, {
x: 4.0,
y: 1
}]
}, {
data: [{
x: 0.2,
y: 0
}, {
x: 1.2,
y: 1
}, {
x: 2.2,
y: 1
}, {
x: 3.2,
y: 1
}, {
x: 4.2,
y: 2
}]
}, {
data: [{
x: 0.2,
y: 5
}, {
x: 1.2,
y: 9
}, {
x: 2.2,
y: 4
}, {
x: 3.2,
y: 5
}, {
x: 4.2,
y: 3
}]
}]
});
#container {
min-width: 300px;
max-width: 800px;
height: 300px;
margin: 1em auto;
}
<script src="http://code.jquery.com/jquery-git.js"></script>
<script src="http://code.highcharts.com/highcharts.js"></script>
<script src="http://code.highcharts.com/modules/exporting.js"></script>
<div id="container"></div>
If you want to show visible series' in the tooltip only, change
// This one already exist
if (series == point.series) return;
to
// This one already exist
if (series == point.series || series.visible==false) return;
Thanks for you solution!!!
for constant order the tooltips
Highcharts.wrap(Highcharts.Tooltip.prototype, 'refresh', function (proceed) {
var args = arguments,
points = args[1],
point = points[0],
chart = point.series.chart;
// Loop over all the series of the chart
Highcharts.each(chart.series, function (series) {
// This one already exist
if (series == point.series || series.visible == false)
return;
var current,
dist,
distance = Number.MAX_VALUE;
// Loop over all the points
Highcharts.each(series.points, function (p) {
// use the distance in X to determine the closest point
dist = Math.abs(p.x - point.x);
if (dist < distance) {
distance = dist;
current = p;
return;
}
});
// Add the closest point to the array
if (points.indexOf(current) == -1)
points.push(current);
});
// for not changing the tooltip series order
var tt = [].slice.call(args, 1);
tt[0].sort(function (a, b) {
if (a.color < b.color)
return -1;
if (a.color > b.color)
return 1;
return 0;
});
proceed.apply(this, tt);
});
Don't forget tooltip option shared!
options = {
tooltip: {
shared: true,
....

Vertical line on highchart, with position tied to animation frame from another div?

I have a 100-frame animation in one div and a standard area highchart in another div, with 100 positions on the x-axis. On the chart I can display a vertical line that tracks mouseovers, using this code:
tooltip: {
shared: true,
crosshairs: true
}
I'd like to create the identical type of line but have its placement tied to the animation frame. That is, as the animation progresses, the line on the chart would move to the matching position (if the animation is on frame 33, the line would move to position 33 on the x-axis of the chart).
How can I make this happen?
I'd like to simply update the value of the plotLine rather than add/remove each time, but I don't see an Axis.updatePlotLine or equivalent. If there is a way to do that, please let me know!
You could a second series as a vertical line and then manipulate that series with a setTimeout and setData call to match the frame speed of your animation (or even better trigger the moving of the line from the animation as it advances to the next frame).
See fiddle here.
$(function () {
var someData = [];
var maxY = -9999, minY = 9999;
for (var i = 0; i < 60; i++)
{
var x = i;
var y = Math.random() * 10;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
someData.push([x,y]);
}
chart = new Highcharts.Chart({
chart: {
renderTo: 'container'
},
xAxis: {
minPadding: 0.05,
maxPadding: 0.05
},
yAxis: {min: minY, max: maxY},
series: [{
data: someData
},
{
data: [[0,minY],[0,maxY]]
}]
});
moveLine = function(){
if (chart.series[1].data[0].x == 59){
x = 0;
}else{
x = chart.series[1].data[0].x + 1;
}
chart.series[1].setData([[x,minY],[x,maxY]]);
setTimeout(moveLine,1000);
}
setTimeout(moveLine,1000);
});​
You can use plotLines like you have discovered, but can avoid the add/remove line approach and rely on Highchart's renderer to animate an existing line. See this fiddle.
Relevant code:
$(function () {
window.chart_instance = new Highcharts.Chart({
yAxis: {
plotLines: [{
color: '#777',
value: 55,
width: 1
}]
},
chart: {
type: 'bar',
renderTo: $('#container')[0]
},
series: [{
data: [29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4]
}]
});
$("#btn").click(function(){
var chart = chart_instance;
var axis = chart.yAxis[0]; // Get a reference to the Axis object that your plotline is associated with
var line = axis.plotLinesAndBands[0]; // Get a reference to the plotLine
line.options.value = 190; // Set desired new value
line.render(); // Render with updated values. Will animate if animation enabled for the chart.
});
});
I was able to make this work using plotLines (http://www.highcharts.com/ref/#xAxis-plotLines):
function addPlotLine(ts) {
chart.xAxis[0].addPlotLine({
value: ts,
color: 'rgb(255, 0, 0)',
width: 1,
id: 'tsline'
});
}
function removePlotLine() {
chart.xAxis[0].removePlotLine('tsline');
}
function doAnimation(ts) {
// animation code here
removePlotLine();
addPlotLine(ts);
}

How to remove the y axis ticks in flot

I am using flot to generate bar graphs.
Here is my code bar graph code
I need to make the y axis tick to disappear.
I need to put some label on the top of each bar
How to do it?
Okay, after a lot of mucking around with Flot and downloading the source, I finally figured out a good starting point for you.
The jsFiddle demo is here.
The guts of the code is using a hook for drawSeries which draws the label:
function drawSeriesHook(plot, canvascontext, series) {
var ctx = canvascontext,
plotOffset = plot.offset(),
labelText = 'TEST', // customise this text, maybe to series.label
points = series.datapoints.points,
ps = series.datapoints.pointsize,
xaxis = series.xaxis,
yaxis = series.yaxis,
textWidth, textHeight, textX, textY;
// only draw label for top yellow series
if (series.label === 'baz') {
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
ctx.lineWidth = series.bars.lineWidth;
ctx.fillStyle = '#000'; // customise the colour here
for (var i = 0; i < points.length; i += ps) {
if (points[i] == null) continue;
textWidth = ctx.measureText(labelText).width; // measure how wide the label will be
textHeight = parseInt(ctx.font); // extract the font size from the context.font string
textX = xaxis.p2c(points[i] + series.bars.barWidth / 2) - textWidth / 2;
textY = yaxis.p2c(points[i + 1]) - textHeight / 2;
ctx.fillText(labelText, textX, textY); // draw the label
}
ctx.restore();
}
}
See the comments for where you can customise the label.
To remove the y-axis ticks, that is just a simple option setting. In addition, you can work out the maximum y-value for each of the bar stacks and then add about 100 to that to set a maximum Y value that will allow for the space taken up by the labels. The code for all of that then becomes:
// determine the max y value from the given data and add a bit to allow for the text
var maxYValue = 0;
var sums = [];
$.each(data,function(i,e) {
$.each(this.data, function(i,e) {
if (!sums[i]) {
sums[i]=0;
}
sums[i] += this[1]; // y-value
});
});
$.each(sums, function() {
maxYValue = Math.max(maxYValue, this);
});
maxYValue += 100; // to allow for the text
var plot = $.plot($("#placeholder"), data, {
series: {
stack: 1,
bars: {
show: true,
barWidth: 0.6,
},
yaxis: {
min: 0,
tickLength: 0
}
},
yaxis: {
max: maxYValue, // set a manual maximum to allow for labels
ticks: 0 // this line removes the y ticks
},
hooks: {
drawSeries: [drawSeriesHook]
}
});
That should get you started. You can take it from here, I'm sure.

Categories

Resources