I have been playing a lot with Chart.Js but trying my hardest to avoid getting into Canvas itself due to time constraints and a personal preference of the SVG route of D3 et al.
I have a mixture of charts on a dashboard page, and everything looks fantastic except for one issue - you have to hover over a pie segment in order to see the underlying % or value.
For a dashboard view, my users would prefer to just quickly see some data labels on the segments - as with Excel - possibly easier to explain with an image:
https://support.office.com/en-gb/article/Display-or-hide-data-label-leader-lines-in-a-pie-chart-d7e7c62e-aaa5-483a-aa00-6d2c437df61b
The problem with other solutions I've found here are that they are simply displaying the value in the segment, but some segments are too small for this to be the way forward.
There were also other solutions that always displayed tooltips - but there was a lot of overlapping and generally looked quite horrible.
I would even be happy if the legend could display data next to it, but I don't understand why a lot more people haven't requested the same functionality - am I missing something?
This feature isn't available so far, so there is no really quick solution for that.
It actually is possible to show the data within the legend (I have done this for dashboards I create at work). You just need to use the generateLabels legend label property to achieve this.
Here is an example that shows the data value in parenthesis within the legend (this is done in the legend item text property that is returned from the function).
generateLabels: function(chart) {
var data = chart.data;
if (data.labels.length && data.datasets.length) {
return data.labels.map(function(label, i) {
var meta = chart.getDatasetMeta(0);
var ds = data.datasets[0];
var arc = meta.data[i];
var custom = arc.custom || {};
var getValueAtIndexOrDefault = Chart.helpers.getValueAtIndexOrDefault;
var arcOpts = chart.options.elements.arc;
var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor);
var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor);
var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth);
return {
// here is where we are adding the data values to the legend title
text: label + " (" + ds.data[i].toLocaleString() + ")",
fillStyle: fill,
strokeStyle: stroke,
lineWidth: bw,
hidden: isNaN(ds.data[i]) || meta.data[i].hidden,
index: i // extra data used for toggling the correct item
};
});
} else {
return [];
}
}
You can see it in action at this codepen.
Related
I am trying to make amCharts V4 to show tooltip for the data item most close to the cursor.
The problem is that my chart is mostly oriented vertically. I was able to break amCharts V4 sample by updating the sample data to get a vertical chart: https://codepen.io/fvnever/pen/jOwyQEE
Here, the data tooltips aren't changing as I move my cursor over the Y axis, and I want them to be updated. I.e. on the following image, it should show the tooltip closest to the cursor from series 1, and it instead shows some item from the beginning of said series.
I've tried various suggestions from the documentation: setting and not setting chart.cursor.xAxis, chart.cursor.yAxis, chart.cursor.snapToSeries. Nothing seems to work.
There's no such feature out of the box, but it's possible to implement one. Two steps:
Make sure to disable the default tooltip behavior:
chart.cursor.snapToSeries = [];
Now, handle the 'cursorpositionchanged' event and control the tooltip there, something like the following.
Please note that this examples is very inefficient (it will iterate over all the chart nodes on every mouse move), so for any practical use, it would be better to store the data points in some sort of precalculated k-d tree.
chart.cursor.events.on('cursorpositionchanged', (e) => {
const cursor = e.target;
const cursorPoint = cursor.point;
let closestSeries = null;
let closestItem = null;
let minimalDistance = null;
for (const series of chart.series) {
for (const dataItem of series.dataItems) {
const dataPoint = dataItem.point;
const distance = Math.sqrt(Math.pow(cursorPoint.x - dataPoint.x, 2) + Math.pow(cursorPoint.y - dataPoint.y, 2));
if (minimalDistance === null || distance < minimalDistance) {
minimalDistance = distance;
closestItem = dataItem;
closestSeries = series;
}
}
}
for (const series of chart.series)
series.tooltip.disabled = series !== closestSeries;
if (closestItem)
closestSeries.showTooltipAtDataItem(closestItem);
});
Full example for testing: https://codepen.io/fvnever/pen/yLXbEKG
I am using the Highcharts synchronized charts to display three different variables. However, in order to render cleaner graphs, I'd like to display the x-axis (which is the same for all three graphs) only for the bottom graph.
For that, I presume, I need to cycle at the end of the generation process through the charts and suppress the first two x-axis, kind like
for (i = 0; i < (Highcharts.charts.length - 1); i = i + 1)
{
chart = Highcharts.charts[i];
chart.xAxis.labels.enabled = false;
}
Here is the default fiddle.
I don't succeed in getting this to work. Can anyone help me out on this?
You can set the xAxis.visible property depending on the chart index:
success: function (activity) {
activity = JSON.parse(activity);
activity.datasets.forEach(function (dataset, i) {
...
Highcharts.chart(chartDiv, {
xAxis: {
visible: i === 2,
...
},
...
});
});
}
Live demo: https://jsfiddle.net/BlackLabel/cmdb5at0/
API Reference: https://api.highcharts.com/highcharts/xAxis.visible
I've created a stacked area chart in Highcharts, which you can see in the image below and in the following jsfiddle: http://jsfiddle.net/m3dLtmoz/
I have a workaround for the gaps you see, which is to group the data for each series by month so that each series looks something like this instead:
series: [{
data: [
[1464739200000,2471],
[1467331200000,6275],
[1470009600000,2574],
[1472688000000,7221],
[1475280000000,3228]
]}
]
While the above isn't exactly what I'm going for, the way the series above is structured does give me what I ultimately want, which is this:
I'm really dying to know why the original setup isn't working appropriately, however. I've tested other instances where datetimes group and aggregate properly based on a single datetime x axis value. I'm stumped as to why this particular data set isn't working. I've tried using the dataGrouping option in the Highstock library, but wasn't able to integrate that effectively. I've messed with options as far as tickInterval goes to no avail. I tried setting the "stacking: 'normal' option in each series instead of in the plotOptions, but that made no difference. I've seen issues on github dealing with the stacked area charts, but nothing seems to exactly match up with what I'm seeing. Any help is appreciated - thank you much!
You receive the error in the console. Most of the series require data to be sorted in ascending order. Stacking has nothing do to it, see example.
Series which do not require data to be sorted are scatter or polygon. No error in scatter
You should sort and group the points on your own. If you want to group them by months you have to prepare the data before you put them in a chart. The example below takes averages from the same datetime.
function groupData(unsortedData) {
var data = unsortedData.slice();
data.sort(function (a, b) {
return a[0] - b[0]
});
var i = 1,
len = data.length,
den = 1,
sum = data[0][1],
groupedData = [[data[0][0], sum]],
groupedData = [];
for (; i < len; i++) {
if (data[i - 1][0] === data[i][0]) {
sum += data[i][1];
den++;
} else {
groupedData.push([data[i - 1][0], sum / den]);
den = 1;
sum = data[i][1];
}
}
groupedData.push([data[i-1][0], sum / den]);
return groupedData;
}
example: http://jsfiddle.net/e4enhw9a/1/
Good morning. I'm working with Chart.js and have it set up to produce a nice bar chart. Unfortunately, the default popup when you hover over the bars only shows the color of the bar and it's value. The color of the bar is unhelpful to our users. I would like the popup to display the label of each bar in the group and its value. Sort of like:
First Project : 80
Second project : 25
Third project : 64
There have been other posts about this issue and they recommended using
multiTooltipTemplate : "<%%=datasetLabel%> : <%%=value%>"
I tried it but it had no effect. No errors but no change to the popup. Perhaps it isn't set up correctly. Any suggestions?
$.post(url, dataObject)
.done(function (results) {
if (results.length > 0) {
//Build mydatasets empty array
var mydatasets = [];
//Loop through the results and build a new data array object to be added to the master data array object that will
//be assigned to the chart bars.
for (var i = 0; i < results.length; i++) {
var dataset =
{
label : results[i].WorkTypeName,
fillColor : getRandomColor(), //Found in the ChartHelpers.js file
data : results[i].Hours
}
mydatasets.push(dataset);
}
//Plug the data into the data structure
var data = {
labels: startdates,
datasets: mydatasets
};
//Get chart context
var ctx = $("#myChart").get(0).getContext("2d");
// This will get the first returned node in the jQuery collection and use it to create a bar chart
myBarChart = new Chart(ctx, {
options: {
multiTooltipTemplate : "<%%=datasetLabel%> : <%%=value%>"
}
}).Bar(data);
}
});
Found the answer with some additional experimentation! Other forum posts had suggested using
multiTooltipTemplate : "<%%=datasetLabel%> : <%%=value%>"
in the options. that generated a syntax error message. Removing the extra two percent signs caused the popup to display as planned.
multiTooltipTemplate : "<%=datasetLabel%> : <%=value%>"
I have a group of graphs visualizing a bunch of data for me (here), based off a csv with approximately 25,000 lines of data, each having 12 parameters. However, doing any interaction (such as selecting a range with the brush on any of the graphs) is slow and unwieldy, completely unlike the dc.js demo found here, which deals with thousands of records as well but maintains smooth animations, or crossfilter's demo here which has 10 times as many records (flights) as I do.
I know the main resource hogs are the two line charts, since they have data points every 15 minutes for about 8 solid months. Removing either of them makes the charts responsive again, but they're the main feature of the visualizations, so is there any way I can make them show less fine-grained data?
The code for the two line graphs specifically is below:
var lineZoomGraph = dc.lineChart("#chart-line-zoom")
.width(1100)
.height(60)
.margins({top: 0, right: 50, bottom: 20, left: 40})
.dimension(dateDim)
.group(tempGroup)
.x(d3.time.scale().domain([minDate,maxDate]));
var tempLineGraph = dc.lineChart("#chart-line-tempPer15Min")
.width(1100).height(240)
.dimension(dateDim)
.group(tempGroup)
.mouseZoomable(true)
.rangeChart(lineZoomGraph)
.brushOn(false)
.x(d3.time.scale().domain([minDate,maxDate]));
Separate but relevant question; how do I modify the y-axis on the line charts? By default they don't encompass the highest and lowest values found in the dataset, which seems odd.
Edit: some code I wrote to try to solve the problem:
var graphWidth = 1100;
var dataPerPixel = data.length / graphWidth;
var tempGroup = dateDim.group().reduceSum(function(d) {
if (d.pointNumber % Math.ceil(dataPerPixel) === 0) {
return d.warmth;
}
});
d.pointNumber is a unique point ID for each data point, cumulative from 0 to 22 thousand ish. Now however the line graph shows up blank. I checked the group's data using tempGroup.all() and now every 21st data point has a temperature value, but all the others have NaN. I haven't succeeded in reducing the group size at all; it's still at 22 thousand or so. I wonder if this is the right approach...
Edit 2: found a different approach. I create the tempGroup normally but then create another group which filters the existing tempGroup even more.
var tempGroup = dateDim.group().reduceSum(function(d) { return d.warmth; });
var filteredTempGroup = {
all: function () {
return tempGroup.top(Infinity).filter( function (d) {
if (d.pointNumber % Math.ceil(dataPerPixel) === 0) return d.value;
} );
}
};
The problem I have here is that d.pointNumber isn't accessible so I can't tell if it's the Nth data point (or a multiple of that). If I assign it to a var it'll just be a fixed value anyway, so I'm not sure how to get around that...
When dealing with performance problems with d3-based charts, the usual culprit is the number of DOM elements, not the size of the data. Notice the crossfilter demo has lots of rows of data, but only a couple hundred bars.
It looks like you might be attempting to plot all the points instead of aggregating them. I guess since you are doing a time series it may be unintuitive to aggregate the points, but consider that your plot can only display 1100 points (the width), so it is pointless to overwork the SVG engine plotting 25,000.
I'd suggest bringing it down to somewhere between 100-1000 bins, e.g. by averaging each day:
var daysDim = data.dimension(function(d) { return d3.time.day(d.time); });
function reduceAddAvg(attr) {
return function(p,v) {
if (_.isLegitNumber(v[attr])) {
++p.count
p.sums += v[attr];
p.averages = (p.count === 0) ? 0 : p.sums/p.count; // gaurd against dividing by zero
}
return p;
};
}
function reduceRemoveAvg(attr) {
return function(p,v) {
if (_.isLegitNumber(v[attr])) {
--p.count
p.sums -= v[attr];
p.averages = (p.count === 0) ? 0 : p.sums/p.count;
}
return p;
};
}
function reduceInitAvg() {
return {count:0, sums:0, averages:0};
}
...
// average a parameter (column) named "param"
var daysGroup = dim.group().reduce(reduceAddAvg('param'), reduceRemoveAvg('param'), reduceInitAvg);
(reusable average reduce functions from the FAQ)
Then specify your xUnits to match, and use elasticY to auto-calculate the y axis:
chart.xUnits(d3.time.days)
.elasticY(true)