I'm trying to update an existing data series with a new data array and invoke the redraw function when done. While this works perfectly, I'm not quite satisfied as I'd like to have a sort of grow/shrink transition. I have seen an example by Highcharts (fiddle around with the existing data set then click on the button "Set new data to selected series") but I can't replicate this behavior.
This is what code that I've written:
var series, newSeriesThreshold = this.chart.series.length * 2;
for (var i = 0; i < data.length; i += 2) {
series = {
name: this.data[i].title,
data: this.data[i].data,
color: this.data[i].color
};
if (i >= newSeriesThreshold) {
this.chart.addSeries(series, false);
} else {
var currentSeries = this.chart.series[i / 2];
currentSeries.setData(series.data, false);
}
}
this.chart.redraw();
These are the options when creating the chart:
var config = {
chart: {
renderTo: $(this.container).attr('id'),
type: this.settings.type,
animation: {
duration: 500,
easing: 'swing'
}
},
title: {
text: null
},
legend: {
enabled: this.settings.legend.show
},
tooltip: {
formatter: function() {
return this.x.toFixed(0) + ": <b>" + this.y.toString().toCurrency(0) + '</b>';
}
},
xAxis: {
title: {
text: this.settings.xaxis.title,
style: {
color: '#666'
}
}
},
yAxis: {
title: {
text: this.settings.yaxis.title,
style: {
color: '#666'
}
}
},
series: series,
plotOptions: {
column: {
color: '#FF7400'
}
},
credits: {
enabled: false
}
};
This yields an immediate update without transitioning effects. Any ideas what I might be doing wrong?
I have solved this problem by destroying and creating again the chart.
Here is the link on highcharts forum that helps me : http://forum.highcharts.com/highcharts-usage/animation-on-redraw-t8636/#p93199
The answer comes from the highcharts support team.
$(document).ready(function() {
var chartOptions = {
// Your chart options
series: [
{name: 'Serie 1' , color: '#303AFF', data: [1,1,1,1,1,1,1}
]
};
var chart = new Highcharts.Chart(chartOptions);
$("#my_button").click(function(){
chart.destroy();
chartOptions.series[0].data = [10,5,2,10,5,2,10];
chart = new Highcharts.Chart(chartOptions);
});
});
This remain a mystery. I managed to make the axis update which suggests that there is some kind of animation going on, but it's applied only to the axis, and not the columns.
In the end, I've settled with this behavior.
This might help:
var mychart = $("#mychart").highcharts();
var height = mychart.renderTo.clientHeight;
var width = mychart.renderTo.clientWidth;
mychart.setSize(width, height);
to update all charts
$(Highcharts.charts).each(function(i,chart){
var height = chart.renderTo.clientHeight;
var width = chart.renderTo.clientWidth;
chart.setSize(width, height);
});
Related
Ok so I build sync charts with test data. Everything works perfectly. The only problem I have is that I want to export all of them in one PDF but I only have the option to do it seperatly. So I build in a button that gets the charts in an array and exports it to PDF but it does not want to work.
This is the JS i have that builds the charts
function clickModal(test) {
document.getElementById("myModal").style.display = "block";
// Call the AJAX function that start the chart procedures
getAjaxData(test);
}
// HIGHCHART FUNCTIONALITY STARTS HERE
['mousemove', 'touchmove', 'touchstart'].forEach(function (eventType) {
document.getElementById('container').addEventListener(
eventType,
function (e) {
var chart,
point,
i,
event;
for (i = 0; i < Highcharts.charts.length; i = i + 1) {
chart = Highcharts.charts[i];
// Find coordinates within the chart
event = chart.pointer.normalize(e);
// Get the hovered point
point = chart.series[0].searchPoint(event, true);
if (point) {
point.highlight(e);
}
}
}
);
});
/**
* Override the reset function, we don't need to hide the tooltips and
* crosshairs.
*/
Highcharts.Pointer.prototype.reset = function () {
return undefined;
};
/**
* Highlight a point by showing tooltip, setting hover state and draw crosshair
*/
Highcharts.Point.prototype.highlight = function (event) {
event = this.series.chart.pointer.normalize(event);
this.onMouseOver(); // Show the hover marker
this.series.chart.tooltip.refresh(this); // Show the tooltip
this.series.chart.xAxis[0].drawCrosshair(event, this); // Show the crosshair
};
/**
* Synchronize zooming through the setExtremes event handler.
*/
function syncExtremes(e) {
var thisChart = this.chart;
if (e.trigger !== 'syncExtremes') { // Prevent feedback loop
Highcharts.each(Highcharts.charts, function (chart) {
if (chart !== thisChart) {
if (chart.xAxis[0].setExtremes) { // It is null while updating
chart.xAxis[0].setExtremes(
e.min,
e.max,
undefined,
false,
{ trigger: 'syncExtremes' }
);
}
}
});
}
}
function getAjaxData(test){
$.getJSON('datasync.php', function(chartData, tstSuccess) {
// Display the results of the getJSON call to data.php
document.getElementById("json1").innerHTML = JSON.stringify(tstSuccess, undefined, 2);
document.getElementById("json").innerHTML = JSON.stringify(chartData, undefined, 2);
var charts1 = {}; // global variable
var index = 0;
// Loop through each dataset in the returned JSON
chartData.datasets.forEach(function (dataset, i) {
// Add X values
dataset.data = Highcharts.map(dataset.data, function (val, j) {
return [chartData.xData[j], val];
});
// Create a child div for each dataset that is returned
var chartDiv = document.createElement('div');
chartDiv.className = 'chart';
document.getElementById('container').appendChild(chartDiv);
charts1[index] = Highcharts.chart(chartDiv, {
chart: {
marginLeft: 40, // Keep all charts left aligned
spacingTop: 20,
spacingBottom: 20
},
exporting: {
buttons: {
contextButton: {
menuItems: [
'downloadPDF',
'viewData'
]
}
}
},
title: {
text: dataset.name,
align: 'left',
margin: 0,
x: 30
},
credits: {
enabled: false
},
legend: {
enabled: false
},
xAxis: {
crosshair: true,
events: {
setExtremes: syncExtremes
},
labels: {
//format: '{value} km'
format: ' '
}
},
yAxis: {
title: {
text: null
}
},
tooltip: {
positioner: function () {
return {
// right aligned
x: this.chart.chartWidth - this.label.width - 40,
y: 10 // align to title
};
},
borderWidth: 0,
backgroundColor: 'none',
pointFormat: '{point.y}',
headerFormat: '',
shadow: false,
style: {
fontSize: '18px'
},
valueDecimals: dataset.valueDecimals
},
series: [{
data: dataset.data,
name: dataset.name,
type: dataset.type,
color: Highcharts.getOptions().colors[i],
fillOpacity: 0.3,
tooltip: {
valueSuffix: ' ' + dataset.unit
}
}]
});
});
index++;
});
}
$('#export-pdf').click(function() {
Highcharts.exportCharts([charts1[0], charts1[1]], {
type: 'application/pdf'
});
});
And this is my HTML
<div id="myModal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<div id="container">
<button id="export-png">Export to PNG</button>
</div>
</div>
</div>
So im using PHP to build up JSON from an array ( will use SQL to build from database later just want to get the functionality working )
Then I use the highcharts library along with JS to display the charts
Ok so I figured out the problem. I brought in this piece of code
Highcharts.getSVG = function(charts) {
var svgArr = [],
top = 0,
width = 0;
$.each(charts, function(i, chart) {
var svg = chart.getSVG();
svg = svg.replace('<svg', '<g transform="translate(0,' + top + ')" ');
svg = svg.replace('</svg>', '</g>');
svg = svg.replace('-9000000000', '-999'); // Bug in v4.2.6
top += chart.chartHeight;
width = Math.max(width, chart.chartWidth);
svgArr.push(svg);
});
return '<svg height="'+ top +'" width="' + width + '" version="1.1" xmlns="http://www.w3.org/2000/svg">' + svgArr.join('') + '</svg>';
};
/**
* Create a global exportCharts method that takes an array of charts as an argument,
* and exporting options as the second argument
*/
Highcharts.exportCharts = function(charts, options) {
// Merge the options
options = Highcharts.merge(Highcharts.getOptions().exporting, options);
// Post to export server
Highcharts.post(options.url, {
filename: options.filename || 'chart',
type: options.type,
width: options.width,
svg: Highcharts.getSVG(charts)
});
};
I changed the charts so that it does not created an array of chart objects
Highcharts.chart(chartDiv, {
chart: {
marginLeft: 40, // Keep all charts left aligned
spacingTop: 20,
spacingBottom: 20
},
instead of
charts1[index] = Highcharts.chart(chartDiv, {
chart: {
marginLeft: 40, // Keep all charts left aligned
spacingTop: 20,
spacingBottom: 20
},
And in the buttons function I just used the highcharts.charts instead of the array
$('#export-pdf').click(function() {
Highcharts.exportCharts(Highcharts.charts, {
type: 'application/pdf'
});
});
I have a zoomable area chart and an x-axis label for every datapoint. When the chart loads, there is way too many labels, therefor I set the step option:
categoryAxis: {
name: 'CatAxis',
categories: graphLabels,
step: 30
}
But when the user zooms in, I need to decrease the amount of steps, otherwise no labels will be shown at all.
Therefor I subscribed to the zoomEnd event and calculate the desired amount of step:
function onZoomEnd(e) {
var xRange = e.axisRanges.CatAxis;
if (xRange) {
var diff = xRange.max - xRange.min;
var step = 1;
while (diff / step > 6) {
step++;
}
e.sender.setOptions({ categoryAxis: { labels: { step: step } } });
}
But setting the options here causes the chart to refresh and thereby losing its zoom level. The ultimate goal is to show a reasonable amount of labels without them overlapping or disappearing when zooming in and out. Any ideas how to achieve this?
you can maintain the zoom level of the chart using the following example from documentation
https://docs.telerik.com/kendo-ui/knowledge-base/maintain-pan-zoom-state
<button id="rebind">Rebind Chart</button>
<div id="chart"></div>
<script>
// Sample data
var data = [];
for (var i = 0; i < 100; i++) {
var val = Math.round(Math.random() * 10);
data.push({
category: "C" + i,
value: val
});
}
function createChart() {
var axisMin = 0;
var axisMax = 10;
function updateRange(e) {
var axis = e.sender.getAxis('axis')
var range = axis.range()
axisMin = range.min;
axisMax = range.max;
}
function restoreRange(e) {
e.sender.options.categoryAxis.min = axisMin;
e.sender.options.categoryAxis.max = axisMax;
}
$("#chart").kendoChart({
renderAs: "canvas",
dataSource: {
data: data
},
categoryAxis: {
name: "axis",
min: axisMin,
max: axisMax,
labels: {
rotation: "auto"
}
},
series: [{
type: "column",
field: "value",
categoryField: "category"
}],
pannable: {
lock: "y"
},
zoomable: {
mousewheel: {
lock: "y"
},
selection: {
lock: "y"
}
},
zoom: updateRange,
drag: updateRange,
dataBound: restoreRange
});
}
$("#rebind").click(function() {
$("#chart").data("kendoChart").dataSource.read();
});
$(document).ready(createChart);
</script>
I have an Google Annotation Chart to show relative performance of different investments. User should be able compare performance over a selected time frame, that is, the series values should be rebased / indexed to 100 at the startdate of the visible range of the chart once the timeframe is changed.
Other packages like Amcharts offer a "comparable" function, so have been looking for options like "scaleColumns" and "scaleType" in Google Docs and in other questions here, not finding any clue on how to do this.
Is there any feature I can use and might have missed, or what would be the best approach to recalculate the DataTable with index=100 upon "rangechange".
Code and screenshot is below:
google.charts.load('current', {
packages: ['corechart', 'line', 'table','annotationchart']
});
google.charts.setOnLoadCallback(drawChart);
// ---------- Chart ---------------------------- //
function drawChart() {
//data query
var query = new google.visualization.Query(
'https://docs.google.com/spreadsheets/d/1Gw67zHpKyEd1nu_V698yqYqNgE0x21_ZE_QDHJmsgtE/gviz/tq?gid=803335131&headers=1&range=A1:n451');
query.send(handleQueryResponse);
}
function handleQueryResponse(response) {
if (response.isError()) {
alert('Error in query: ' + response.getMessage() + ' ' + response.getDetailedMessage());
return;
}
var data = response.getDataTable();
var options = {
title: 'Development of the choosen Portfolio since 2008',
legend: {
textStyle : { fontSize: 8 },
maxLines : 2,
position: 'top'
},
width: '30%',
height: 700,
lineWidth: 1,
hAxis: {
format: 'M/d/yy',
title: 'Time'
},
vAxis: {
scaleType: 'log',
title: 'Return (log scale)'
},
//theme: 'maximized',
chartArea:{
left:0,
top:20,
width:'30%',
height:'85%'
},
series: {
22: {
lineWidth: 3,
color: 'red'
}
}
};
var chart = new google.visualization.AnnotationChart(document.getElementById('chart_div'))
chart.draw(data, options);
}
google.visualization.events.addListener(chart, 'rangechange', rangechange_handler);
function rangechange_handler(e) {
console.log('You changed the range to ', e['start'], ' and ', e['end']);
// How to recalculate datatable to keep index=100 for all series upon rangechange?
}
Update:
There is a way, working on it: https://groups.google.com/forum/#!searchin/google-visualization-api/compare$20zoom|sort:relevance/google-visualization-api/8HjybllsufY/z5uak6AymLcJ
After an extended period of pain got it working, hope it helps somebody: https://jsfiddle.net/AlexHorn/a9z15syr/
var viewColumns = [0];
var colors = [];
for (var i = 0; i < columnIndices.length; i++) {
viewColumns.push({
label: data.getColumnLabel(columnIndices[i]),
type: 'number',
calc: (function(x) {
// use a closure here to lock the value of i to each column
return function(dt, row) {
// return the value normalized to the first row in the view
return dt.getValue(row, columnIndices[x]) / dt.getValue(0, columnIndices[x]);
};
})(i)
});
}
Aim: I have two identical Highcharts treemaps and if I select one point of one chart, I also want to send a select event to the second chart's point with the same id/ same position.
Progress:
I followed these answers that tackle the same problem, only for line graphs, and then adopted the fiddle posted there for treemaps. You can find it here as well as below:
$(function () {
var prevPid = 0;
var chart = {
plotOptions: {
series: {
allowPointSelect: true,
point: {
'events': {
select: function () {
var pId = this.series.data.indexOf(this);
var chart1 = $('#container').highcharts();
var chart2 = $('#container2').highcharts();
chart1.series[0].data[pId].setState('select');
chart2.series[0].data[pId].setState('select');
chart2.series[0].data[prevPid].setState('');
prevPid = pId;
}
}
}
}
},
series: [{
type: "treemap",
data: [{
name: 'A',
value: 6,
colorValue: 1
}, {
name: 'B',
value: 6,
colorValue: 2
}, {
name: 'C',
value: 4,
colorValue: 3
}]
}]
};
$('#container').highcharts(chart);
$('#container2').highcharts(chart);
});
Problem: However the corresponding point of the other chart is not selected. Any advice on how to fix it?
Highcharts v5+
Instead of playing around with states, it's easier to use point.update(), demo: http://jsfiddle.net/BlackLabel/hy12z5u7/
Settings:
chart: {
events: {
load: function() {
$.each(this.series, function(i, s) {
$.each(s.data, function(j, p) {
p.pointAttr = {
select: {
color: "red"
}
};
});
});
}
}
},
And action:
point: {
'events': {
click: function() {
var pId = this.series.data.indexOf(this);
var chart1 = $('#container').highcharts();
var chart2 = $('#container2').highcharts();
chart1.series[0].data[prevPid].update({
color: chart1.series[0].color
});
chart2.series[0].data[prevPid].update({
color: chart2.series[0].color
});
chart1.series[0].data[pId].update({
color: chart1.series[0].data[pId].pointAttr.select.color
});
chart2.series[0].data[pId].update({
color: chart2.series[0].data[pId].pointAttr.select.color
});
prevPid = pId;
}
}
}
Highcharts < v4
The problem is that treemap doesn't have states.select option (API: http://api.highcharts.com/highcharts#plotOptions.treemap.states ) so even, when you force select-ed state, then nothing visually changes on your chart. You can add that state manually: http://jsfiddle.net/tqa6uxdb/3/
chart: {
events: {
load: function() {
$.each(this.series, function(i, s) {
$.each(s.data, function(j, p) {
p.pointAttr.select = {
fill: "red"
}
});
});
}
}
},
Note: your logic for selecting/deselecting points is missing checking if currently clicked point is already clicked.
Not sure why because I have done it in the past, but I have a Highcharts bar chart and it won't animate. This is the declaration of the chart,
function initializeData() {
$http.get(url).success(function(ret) {
$scope.jsondata = ret;
var newdata = [];
for (x = 0; x < 5; x++) {
newdata.push({
name: setName($scope.jsondata[x].name),
y: $scope.jsondata[x].data[0],
color: getColor($scope.jsondata[x].data[0])
});
}
$scope.chart.series[0].setData(newdata);
});
mainInterval = $interval(updateData, 5000);
}
function updateData() {
$http.get(url).success(function(ret) {
$scope.jsondata = ret;
console.debug("here");
for (x = 0; x < 5; x++) {
$scope.chart.series[0].data[x].update({
y: $scope.jsondata[x].data[0],
color: getColor($scope.jsondata[x].data[0])
});
}
});
}
$scope.chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'bar',
animation: true,
events: {
load: initializeData
}
},
title: {
text: ''
},
xAxis: {
type: 'category',
labels: {
style: {
fontSize: '11px'
}
}
},
yAxis: {
min: 0,
max: 100,
title: {
text: 'Total Score',
align: 'high'
}
},
legend: {
enabled: false
},
tooltip: {
pointFormat: 'Total Score <b>{point.y:.3f}</b>'
},
series: [{
name: 'Active Users',
data: [],
dataLabels: {
enabled: true,
rotation: 30,
style: {
fontSize: '10px',
fontFamily: 'Verdana, sans-serif'
},
format: '{point.y:.3f}', // one decimal
}
}]
});
And as you can see I have animate : true, so I am not sure what is the problem here. I have this older plunker where all of the data is in separate series, but it animates fine. But this is the plunker I am working on and having trouble with. They are like identical basically. In the newer one I broke out the initialization of data into its own method, but that is the only real main difference.
Some edits:
So as I was saying, I have done things this way with an areaspline chart (I know it was said they work a bit different but they are set up identically).
function initializeData() {
$interval.cancel(mainInterval);
$scope.previousPackets = '';
$http.get("https://api.myjson.com/bins/nodx").success(function(returnedData) {
var newdata = [];
var x = (new Date()).getTime();
for (var step = 9; step >= 0; step--) {
newdata.push([x - 1000 * step, 0]);
}
$scope.chart.series[0].setData(newdata);
});
mainInterval = $interval(updateData, 2000);
}
function updateData() {
$http.get(url + acronym + '/latest').success(function(returnedData) {
var x = (new Date()).getTime();
if ($scope.previousPackets != returnedData[0].numPackets) {
$scope.chart.series[0].addPoint([x, returnedData[0].numPackets], true, true);
$scope.previousPackets = returnedData[0].numPackets;
} else {
$scope.chart.series[0].addPoint([x, 0], true, true);
}
});
}
$scope.chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'areaspline',
animation: Highcharts.svg, // don't animate in old IE
marginRight: 10,
events: {
load: initializeData
}
},
title: {
text: ''
},
xAxis: {
type: 'datetime',
tickPixelInterval: 150
},
yAxis: {
title: {
text: 'Packets'
},
plotLines: [{
value: 0,
width: 1,
color: '#d9534f'
}]
},
tooltip: {
formatter: function() {
return Highcharts.numberFormat(this.y) + ' packets<b> | </b>' + Highcharts.dateFormat('%H:%M:%S', this.x);
}
},
legend: {
enabled: false
},
exporting: {
enabled: false
},
series: [{
name: 'Packets',
data: []
}]
});
I also updated the first chunk of code with the initializeData() method and updateData() method which are seemingly identical in both different charts.
It looks like it plays an important role if you provide your data at chart initialization or after. For simplicity I refactored your code a little
function initializeChart(initialData, onload) {
$scope.chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'bar',
animation: true,
events: {
load: onload
}
....
series: [{
name: 'Active Users',
data: initialData,
dataLabels: {
enabled: true,
format: '{point.y:.3f}', // one decimal
}
}]
});
}
function getData(callback) {
$http.get(url).success(function(ret) {
$scope.jsondata = ret;
var newdata = [];
for (x = 0; x < 5; x++) {
newdata.push([setName(ret[x].name), ret[x].data]);
}
callback(newdata);
});
}
As a result your two planks are in essense reduced to two methods below. The first initializes chart with preloaded data and the second updates data in existing chart.
function readDataFirst() {
getData(function(newdata) {
initializeChart(newdata);
});
}
function initializeChartFirst() {
initializeChart([], function() {
getData(function(newdata) {
$scope.chart.series[0].setData(newdata);
})
});
}
The first one animates fine while the second does not. It looks like highcharts skips animation if dataset is not initial and is treated incompatible.
However if you really want to have animation in your current plant (chart first workflow) you can achieve that by initializing first serie with zeros and then with the real data. This case it will be treated as update
function forceAnimationByDoubleInitialization() {
getData(function(newdata) {
initializeChart([]);
var zerodata = newdata.map(function(item) {
return [item[0], 0]
});
$scope.chart.series[0].setData(zerodata);
$scope.chart.series[0].setData(newdata);
});
All these options are available at http://plnkr.co/edit/pZhBJoV7PmjDNRNOj2Uc