I'm developing an web application that handles and shows large amounts of live data from some devices. To visualise the data I decided to use HighStock. It seems to work well on most of the data:
However, when the bottom navigator touches right border, the picture becomes quite different:
The timeline is almost the same, but the number of points is different, also vertical scale is different... What is this happening? How to fix it?
My code looks this way:
const ch1 = Highcharts.stockChart('chart1', {
rangeSelector: {
selected: 1,
inputEnabled: false,
buttonTheme: {visibility: 'hidden'},
labelStyle: {visibility: 'hidden'},
},
title: {
text: 'Metrics',
},
series: [{
name: 'Sensor 1', data: [],
}, {
name: 'Sensor 2', data: [],
}, {
name: 'Sensor 3', data: [],
}]
});
// a,b,c gets values from the server
// They are arrays of pairs of timestamp & value
ch1.series[0].setData(a);
ch1.series[1].setData(b);
ch1.series[2].setData(c);
// tm_min & tm_max are dynamically calculated using the data
ch1.xAxis[0].setExtremes(tm_min, tm_max);
Update: Here is an example with 2% of my data – try to do the same as shown above.
I found the solution. The issue is caused by your data and xAxis.ordinal that is enabled by default in Highstock. Your data has many empty points on the right side of the chart and because of ordinal, the empty space was not rendered, yet dataGrouping grouped data differently.
Check this here https://jsfiddle.net/BlackLabel/x1tgqbw6/ (disabled ordinal):
xAxis: {
ordinal: true
}
So, the solution is to disable xAxis.ordinal or generate your data without null points:
https://jsfiddle.net/BlackLabel/ex054oy8/
API reference:
https://api.highcharts.com/highstock/xAxis.ordinal
Related
I have a stacked bar chart made with C3.js which uses the following code to be generated:
stacked_bar_chart = c3.generate({
bindto: '#stacked_bar_chart_container',
data: {
columns: [
["Critical", 446, 863],
["High", 1160, 2301],
["Medium", 3106, 8258],
["Low", 277, 119],
["Informational", 7374, 23240]
],
type: 'bar',
groups: [
['Low', 'Medium', 'Informational', 'High', 'Critical', 'Unknown']
],
},
grid: {
y: {
lines: [{ value: 0 }]
}
},
axis: {
x: {
type: 'category',
categories: ["Remediated", "Unconfirmed"] // Notice the x-axis has categories
},
y: {
label: 'Number of Findings'
}
},
});
I am trying to make it so that at the click of a button, I am able to hide the bar called Remediated from the graph. I have tried to unload it by doing the following:
stacked_bar_chart.unload("Remediated");
but this has no effect, and I am pretty sure it is because I am using type: 'category' for the x-axis. I would prefer to not have to unload the data anyways so that later on I can re-display the bar as needed without retrieving the data again.
After some research in the C3.js reference page, I think that there is no easy API function for this to be accomplished, so I have come up with my own tested implementation of this feature that I am currently using.
Firstly, with the way that I do it I am keeping track of three separate global variables which will hold the data currently in the chart and also will hold the data we remove from it. This is the way I decided to choose because the data for my chart is coming from a web resource, so it would be inefficient to keep making AJAX calls and refreshing the data every time a category is added or removed.
// Our three new variables
var removed_from_stacked_bar = {};
var stacked_bar_categories = ["Remediated", "Unconfirmed"];
var stacked_bar_data = [
["Critical", 446, 863],
["High", 1160, 2301],
["Medium", 3106, 8258],
["Low", 277, 119],
["Informational", 7374, 23240]
];
function initialize_stacked_bar_chart(data, categories) {
stacked_bar_chart = c3.generate({
bindto: '#stacked_bar_chart_container',
data: {
columns: data, // Coming from the parameter
type: 'bar',
groups: [
['Low', 'Medium', 'Informational', 'High', 'Critical', 'Unknown']
],
},
grid: {
y: {
lines: [{ value: 0 }]
}
},
axis: {
x: {
type: 'category',
categories: categories // Coming from the parameter
},
y: {
label: 'Number of Findings'
}
},
});
}
initialize_stacked_bar_chart(stacked_bar_data, stacked_bar_categories);
Now I wrote a function called update_stacked_bar_chart() which has a category parameter in order to remove / add the category that is passed in from the chart whenever it is called.
function update_stacked_bar_chart(category) {
var categoryIndex = stacked_bar_categories.indexOf(category);
var removed_values = [];
if (categoryIndex != -1) { // Removing the item since it exists in the bar chart's categories
stacked_bar_categories.splice(categoryIndex, 1); // Removing the category name from the bar chart's category list
stacked_bar_data.forEach(function (item, index) {
var temp = item.splice(categoryIndex + 1, 1); // Removing the value this category held (in-place) in the sublist for each severity
removed_values.push(temp); // Pushing each removed value into the array of removed values (in order from Critical, High, Medium, Low, Informational).
});
removed_from_stacked_bar[category] = removed_values;
} else { // Re-adding the item if it was not found in the current chart's categories
stacked_bar_categories.push(category); // Adding the category name to the bar chart's category list
removed_from_stacked_bar[category].forEach(function (item, index) {
stacked_bar_data[index].push(item); // Adding the value for each severity into the respective severity list
});
delete removed_from_stacked_bar[category];
}
initialize_stacked_bar_chart(stacked_bar_data, stacked_bar_categories); // Remaking the bar chart with the new data and categories.
}
This function will allow you to toggle any category from your bar chart every time it is called. You can attach it to an event listener so that it is called as you need it.
Here is an example of how it can be used to toggle bars as it is called:
update_stacked_bar_chart("Remediated"); // Removes the "Remediated" bar
update_stacked_bar_chart("Remediated"); // Re-adds the "Remediated" bar
update_stacked_bar_chart("Remediated"); // Removes the "Remediated" bar
update_stacked_bar_chart("Unconfirmed"); // Removes the "Unconfirmed" bar
update_stacked_bar_chart("Remediated"); // Re-adds the "Remediated" bar
update_stacked_bar_chart("Unconfirmed"); // Re-adds the "Unconfirmed" bar
I have a data set that contains many fields. I have no control over the creation of this JSON. Sample:
data = [
{
'maparea':'3704000063',
'relatedsource':null,
'empcount':'198390',
'response':'78',
'mean':'61663.00',
},
...
]
The chart code is:
Highcharts.mapChart('container', {
chart: {
map: geojson
},
title: {
text: 'GeoJSON in Highmaps'
},
mapNavigation: {
enabled: true,
buttonOptions: {
verticalAlign: 'bottom'
}
},
colorAxis: {
tickPixelInterval: 100
},
series: [{
data: data,
keys: ['maparea', 'relatedsource', 'empcount', 'response', 'mean'],
joinBy: ['fips', 'maparea'],
name: 'Random data',
states: {
hover: {
color: '#a4edba'
}
},
dataLabels: {
enabled: true,
format: '{point.properties.postal}'
}
}]
});
The geoJSON uses fips to label the areas (in this case counties in NC). The map shows the state and county elements. However, no data is used to plot. This is because the HighMaps code is expecting a value element to be present in the data I think.
Is there a way to tell HighMaps what element in the data set to use to shade the choropleth?
I don't see any option to map your unique data shape to the expected keys in the data according to the docs. Per your comment this is possible with an array, but it doesn't seem to be possible with an object.
However, it's pretty simple to just remap your object to the required shape. The code below gives a partial example.
let dataMapped = data.map(obj => {
var median = Number(obj.median);
return Object.assign(obj, { name: obj.maparea, value: median });
});
And then use dataMapped as the value for your data.
There might be a more elegant way to do this in ES6 with object spread and avoid the Object.assign I am using to merge the old object with new attributes, but I don't have time to research that at the moment.
I know you can pass arbitrary data into your time series points, such as:
new Highcharts.Chart( {
...,
series: [{
name: 'Foo',
data: [ { y : 10.0, customData : 'value 1' },
{ y : 20.0, customData : 'value 2' },
{ y : 30.0, customData : 'value 3' } ]
}]
} );
However, I noticed that this doesn't quite work in HighStock when your time series is comprised of a large data set (1000+ points).
For example, here is a working fiddle http://jsfiddle.net/gparajon/c5fej775/ (less than 1000 points, which also happens to be the default turboThreshold). And here's the same fiddle, with more data, which breaks the tooltip formatter: http://jsfiddle.net/gparajon/5om258az/
Any workaround?
Thanks!
The error in the console is a bug and it is not really connect why you cannot access extra info in the formatter.
The difference between a chart and a stockchart is that a stockchart does data grouping, what means that in the formatter callback you receive grouped points which does not include extra data (how should they be grouped?).
example: https://jsfiddle.net/g04La2qh/1/
If you disable data grouping, you will receive non-grouped points with extra data.
dataGrouping: {
enabled: false
},
example: https://jsfiddle.net/g04La2qh/2/
I need to show an array of objects in the table like representation. Table has columns with the properties, and when clicked on the column it should show more data inside the table. It should be sortable.
Is there a JS library that could do this, so I dont have to write this from scratch?
Please see the attached image with the JSON object.
When the user clicks on Ana, additional row is inserted.
I created the demo https://jsfiddle.net/OlegKi/kc2537ty/1/ which demonstrates the usage of free jqGrid with subgrids. It displays the results like
after the user clicks on the "+" icon in the second line.
The corresponding code you can find below
var mydata = [
{ id: 10, name: "John", lname: "Smith", age: 31, loc: { location: "North America", city: "Seattle", country: "US" } },
{ id: 20, name: "Ana", lname: "Maria", age: 43, loc: { location: "Europe", city: "London", country: "UK" } }
];
$("#grid").jqGrid({
data: mydata,
colModel: [
{ name: "name", label: "Name" },
{ name: "lname", label: "Last name" },
{ name: "age", label: "Age", template: "integer", align: "center" }
],
cmTemplate: { align: "center", width: 150 },
sortname: "age",
iconSet: "fontAwesome",
subGrid: true,
subGridRowExpanded: function (subgridDivId, rowid) {
var $subgrid = $("<table id='" + subgridDivId + "_t'></table>"),
subgridData = [$(this).jqGrid("getLocalRow", rowid).loc];
$("#" + subgridDivId).append($subgrid);
$subgrid.jqGrid({
idPrefix: rowid + "_",
data: subgridData,
colModel: [
{ name: "location", label: "Localtion" },
{ name: "city", label: "City" },
{ name: "country", label: "Country" }
],
cmTemplate: { align: "center" },
iconSet: "fontAwesome",
autowidth: true
});
}
});
Small comments to the code. Free jqGrid saves all properties of input data in data parameter. I added id property to every item of input data. It's not mandatory, but it could be helpful if you would add more functionality to the grid. See the introduction for more details.
The columns are sortable based on the type of the data specified by sorttype property of colModel. To simplify usage some standard types of data free jqGrid provides some standard templates which are shortcurts for some set of settings. I used template: "integer" in the demo, but you could replace it to sorttype: "integer" if only sorting by integer functionality is important.
If the user click on "+" icon to expand the subgrid then jqGrid inserts new row and creates the div for the data part of the subgrid. You can replace subGridRowExpanded from above example to the following
subGridRowExpanded: function (subgridDivId) {
$("#" + subgridDivId).html("<em>simple subgrid data</em>");
}
to understand what I mean. The unique id of the div will be the first parameter of the callback. One can create any common HTML content in the subgrid. Thus one can create empty <table>, append it to the subgrid div and
then convert the table to the subgrid.
To access to the item of data, which corresponds to the expanding row one can use $(this).jqGrid("getLocalRow", rowid). The return data is the item of original data. It has loc property which we need. To be able to use the data as input for jqGrid we create array with the element. I's mostly all, what one have to know to understand how the above code works.
You can add call of .jqGrid("filterToolbar") to be able to filter the data or to add pager: true (or toppager: true, or both) to have the pager and to use rowNum: 5 to specify the number of rows in the page. In the way you can load relatively large set of data in the grid and the user can use local paging, sorting and filtering. See the demo which shows the performance of loading, sorting and filtering of the local grid with 4000 rows and another one with 40000 rows. All works pretty quickly if one uses local paging and not displays all the data at once.
I use datatables.net for all my "more complex than lists"-tables. I It's a very well kept library with loads of features and great flexibility.
In the "con" column I would say that it's so complex that it probably has quite a steep learning curve. Although the documentation is great so there is always hope for most problems.
Depending on the value of the highcharts range selector, I would like to change the data grouping. In my column chart, if one week is selected, there should be 7 bars, if one day is selected, there should be 24 bars, if one month is selected, there should be a bar for each day of the month.
There doesnt seem to be any way to supply a function inside the highchart configs to accomplish this, but I may be missing something.
My current plan was to handle a click event on the range selector to update the series data to contain the correct amount of points. But there may be a better way.
Thanks
There certainly are a bunch of options available in highstock for data grouping.
The primary one that you should look at is units. Here you can specify what kind of groups are allowed.
Top this up with groupPixelWidth and you have what you need, this width defines how small can a point in your chart be, if the number of points on the chart goes higher, the width per point decreases, once it goes below this threshold highcharts would force grouping. Keep this large enough to force grouping of next level, given you want not more than ~30 points on the screen.
dataGrouping: {
units: [
['hour', [1]],
['day', [1]],
['month', [1]],
['year', null]
],
groupPixelWidth: 100
}
#jsFiddle
Instead of using events you can combine range selector buttons with data grouping.
See: "Data grouping by buttons" in the API https://api.highcharts.com/highstock/rangeSelector.buttons
Example: https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/stock/rangeselector/datagrouping/
rangeSelector: {
allButtonsEnabled: true,
buttons: [{
type: 'month',
count: 3,
text: 'Day',
dataGrouping: {
forced: true,
units: [['day', [1]]]
}
}, {
type: 'year',
count: 1,
text: 'Week',
dataGrouping: {
forced: true,
units: [['week', [1]]]
}
}, {
type: 'all',
text: 'Month',
dataGrouping: {
forced: true,
units: [['month', [1]]]
}
}]
},
We tried a Hack around this, where we used Highstock's (Splinechart) RangeSelector, Event and DataGrouping. On click of weekly rangeselectorButton we catch this event through setExtremes. Post catching the event approximate it to "sum". If you are using two series than iterate the object.
events: {
setExtremes: function (e) {
if (e.rangeSelectorButton != undefined) {
var triger = e.rangeSelectorButton;
if (triger.type == 'week') {
$.each(this.series, function (index, obj) {
obj.options.dataGrouping.units[0] = ['week', [1]];
});
} else if (triger.type == 'day') {
$.each(this.series, function (index, obj) {
obj.options.dataGrouping.units[0] = ['day', [1]];
});
}
}
}
},
#fiddle