How to hide bars on a stacked bar chart in C3.js - javascript

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

Related

Adjust Highcharts data grouping based on range selector

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

Highstock setExtremes with a custom range selector button

In highstock range selector I added a custom range selector button (named: my dates) and would like to set a custom extremes when this button is called. I know it works if you put simple button outside the chart and call: chart.xAxis[0].setExtremes(30,80);.
But my scenario is different I want to add a button beside "1m 1y All" range selector buttons, and want that new button to set a custom extremes dates. Using xAxis events setExtremes, does not seems to work unless I am missing something. http://jsfiddle.net/Aeaz3/1/
$(function() {
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=aapl-c.json&callback=?', function(data) {
// Create the chart
$('#container').highcharts('StockChart', {
rangeSelector: {
buttons: [{
type: '',
count: 2,
text: 'My dates'
},{
type: 'hour',
count: 1,
text: '1h'
}, {
type: 'day',
count: 1,
text: '1d'
}, {
type: 'month',
count: 1,
text: '1m'
}, {
type: 'year',
count: 1,
text: '1y'
}, {
type: 'all',
text: 'All'
}],
},
title : {
text : 'AAPL Stock Price'
},
xAxis: {
events:{
setExtremes: function(e) {
var xMin = e.min;
var xMax = e.max;
var zmRange = computeTickInterval(xMin, xMax);
this.chart.xAxis[0].options.tickInterval =zmRange;
this.chart.xAxis[0].isDirty = true;
},
}
},
series : [{
name : 'AAPL',
data : data,
tooltip: {
valueDecimals: 2
}
}]
});
});
});
The setExtremes callback:
Fires when the minimum and maximum is set for the axis, either by
calling the .setExtremes() method or by selecting an area in the
chart. The this keyword refers to the axis object itself. One
parameter, event, is passed to the function. This contains common
event information based on jQuery or MooTools depending on which
library is used as the base for Highcharts.
So it's not really meant to be used to set extremes but is rather a notification when something else does some extreme setting.
That said, I still think it is possible to leverage it for your use case by catching the call when your button is clicked and then resetting it to your custom range:
xAxis: {
events:{
if (e.trigger == "rangeSelectorButton" &&
e.rangeSelectorButton.text == "My dates"){
// it is your button that caused this,
// so setExtrememes to your custom
// have to do in timeout to let
// highcharts finish processing events...
setTimeout(function(){
Highcharts.charts[0].xAxis[0].setExtremes(1198681756385,1368144000000)
}, 1);
}
}
},
Updated Fiddle here.
One approach would be to modify highstock to use the values of e.min and e.max if they are changed in your event handler. This can be done by modifying 3 lines of code.
in highstock.src.js line 7447 (in version 2.0.4). The method is called setExtremes.
Change:
fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
axis.userMin = newMin;
axis.userMax = newMax;
To:
fireEvent(axis, 'setExtremes', eventArguments, function (event) { // the default event handler
axis.userMin = event.min;
axis.userMax = event.max;
And now changing e.min or e.max in the xAxis.setExtremes event will work. (Don't call setExtremes())

How to make a Pie Chart Aggregate the Data Source?

Using Kendo UI Complete for ASP.NET MVC, version: 2013.3 1119 (Nov 20, 2013)...
If I have this bit of code:
$("#status-chart").kendoChart({
dataSource: {
data: [
{Status: 10},
{Status: 20},
{Status: 200},
{Status: 200}
]
},
series: [{
field: 'Status',
categoryField: "Status",
aggregate: 'count'
}]
});
I get this chart:
As you can see - Status 10 and 20 have got a value of 1 and Status 200 a value of 2.
Great, but what I actually want is exactly the same thing in a pie chart (so, a chart with 3 pie slices, 2 of which are exactly the same size and one that is 2 times as big as the rest).
I therefore thought to myself, all I need to do is just set type: "pie" like so:
$("#status-chart").kendoChart({
dataSource: {
data: [
{Status: 10},
{Status: 20},
{Status: 200},
{Status: 200}
]
},
series: [{
field: 'Status',
categoryField: "Status",
aggregate: 'count',
type: "pie"
}]
});
But that produced this chart:
You can see that Status 200 is repeated and the value is determining the size of the slices.
So, here is my question:
How can I create a pie chart that looks like the picture below but which is bound to the data source in the first code snippet above?
Incidentally, the reason I do not want to change the data source is that I wish to share it with a grid.
What you are trying to do here is to group a shared DataSource and have it only affect one widget. Furthermore, Kendo UI will return a grouped object when you group it. The Pie chart is not interested in these objects, but rather the count of the items that each of these group objects contains. We just need to get the data in the right format.
So you have your original DataSource (which I have extracted since it's shared with another widget). When that DataSource changes, you want to populate a second one - one that you can group without affecting the grid.
var ds = new kendo.data.DataSource({
data: [
{Status: 10},
{Status: 20},
{Status: 200},
{Status: 200}
],
change: function() {
chartData.data(this.data());
}
});
The second DataSource (chartData) is grouped, and when it changes, it populates an array, constructing objects that the pie chart can actually understand.
var groupedData = [];
// populate the grouped data array by grouping this datasource
// and then populating an plain array
var chartData = new kendo.data.DataSource({
group: { field: 'Status' },
change: function() {
groupedData = [];
$.each(this.view(), function() {
groupedData.push({ field: this.value, value: this.items.length });
});
}
});
And then just bind your pie chart to that array
$("#status-chart").kendoChart({
dataSource: groupedData,
series: [{
type: 'pie',
field: 'value',
categoryField: 'field'
}]
});
Working example: http://jsbin.com/EKuxORA/1/edit

Render dynamic components in ExtJS 4 GridPanel Column with Ext.create

I've got an ExtJS (4.0.7) GridPanel that I'm populating from a store. The values that I display in the GridPanel's column need to have a different view depending on the type of data that's in the record.
The ultimate goal is that records with "double" or "integer" value for the record's type property present a slider to the user that they can adjust, and a type of "string" just renders some read-only text.
I've created a custom Column to do this. It inspects the type in the renderer and determines what to render.
I've got the "string" working fine with the code below, but struggling with how I can dynamically create and render the more complicated slider control in the column.
This simplified example is just trying to render a Panel with a date control in it as if I can get that going, I can figure out the rest of the slider stuff.
Ext.define('MyApp.view.MyColumn', {
extend: 'Ext.grid.column.Column',
alias: ['widget.mycolumn'],
stringTemplate: new Ext.XTemplate('code to render {name} for string items'),
constructor: function(cfg){
var me = this;
me.callParent(arguments);
me.renderer = function(value, p, record) {
var data = Ext.apply({}, record.data, record.getAssociatedData());
if (data.type == "string") {
return me.renderStringFilter(data);
} else if (data.type == "double" || data.type == "integer") {
return me.renderNumericFilter(data);
} else {
log("Unknown data.type", data);
};
},
renderStringFilter: function(data) {
// this works great and does what I want
return this.stringTemplate.apply(data);
},
renderNumericFilter: function(data) {
// ***** How do I get a component I "create" to render
// ***** in it's appropriate position in the gridpanel?
// what I really want here is a slider with full behavior
// this is a placeholder for just trying to "create" something to render
var filterPanel = Ext.create('Ext.panel.Panel', {
title: 'Filters',
items: [{
xtype: 'datefield',
fieldLabel: 'date'
}],
renderTo: Ext.getBody() // this doesn't work
});
return filterPanel.html; // this doesn't work
}
});
My problem really is, how can I Ext.create a component, and have it render into a column in the gridpanel?
There are a few ways that I have seen this accomplished. Since the grid column is not an Ext container it can not have Ext components as children as part of any configuration the way other container components can. Post grid-rendering logic is required to add Ext components to cells.
This solution modifies your custom column render so that it puts a special css class on the rendered TD tag. After the grid view is ready, the records are traversed and the custom class is found for appropriate special columns. A slider is rendered to each column found.
The code below is a modified version of the ext js array grid example provided in the Sencha examples. The modification mixes in the custom column renderer and the post grid rendering of sliders to TD elements.
This example only includes enough modification of the Sencha example to show the implementation ideas. It lacks separated view and controller logic.
This is modified from here
Ext.require([
'Ext.grid.*',
'Ext.data.*',
'Ext.util.*',
'Ext.data.Model'
]);
Ext.onReady(function() {
// sample static data for the store
Ext.define('Company', {
extend: 'Ext.data.Model',
fields: ['name', 'price', 'change', 'pctChange', 'lastUpdated', 'type']
});
var myData = [
['3m Co', 71.72, 2, 0.03, '9/1/2011', 'integer'],
['Alcoa Inc', 29.01, 4, 1.47, '9/1/2011', 'string'],
['Altria Group Inc', 83.81, 6, 0.34, '9/1/2011', 'string'],
['American Express Company', 52.55, 8, 0.02, '9/1/2011', 'string'],
['American International Group, Inc.', 64.13, 2, 0.49, '9/1/2011', 'integer'],
['AT&T Inc.', 31.61, 4, -1.54, '9/1/2011', 'integer'],
['Boeing Co.', 75.43, 6, 0.71, '9/1/2011', 'string'],
['Caterpillar Inc.', 67.27, 8, 1.39, '9/1/2011', 'integer'],
['Citigroup, Inc.', 49.37, 1, 0.04, '9/1/2011', 'integer'],
['E.I. du Pont de Nemours and Company', 40.48, 3, 1.28, '9/1/2011', 'integer'],
['Exxon Mobil Corp', 68.1, 0, -0.64, '9/1/2011', 'integer'],
['General Electric Company', 34.14, 7, -0.23, '9/1/2011', 'integer']
];
// create the data store
var store = Ext.create('Ext.data.ArrayStore', {
model: 'Company',
data: myData
});
// existing template
stringTemplate = new Ext.XTemplate('code to render {name} for string items');
// custom column renderer
specialRender = function(value, metadata, record) {
var data;
data = Ext.apply({}, record.data, record.getAssociatedData());
if (data.type == "string") {
return stringTemplate.apply(data);;
} else if (data.type == "double" || data.type == "integer") {
// add a css selector to the td html class attribute we can use it after grid is ready to render the slider
metadata.tdCls = metadata.tdCls + 'slider-target';
return '';
} else {
return ("Unknown data.type");
}
};
// create the Grid
grid = Ext.create('Ext.grid.Panel', {
rowsWithSliders: {},
store: store,
stateful: true,
stateId: 'stateGrid',
columns: [{
text: 'Company',
flex: 1,
sortable: false,
dataIndex: 'name'
}, {
text: 'Price',
width: 75,
sortable: true,
renderer: 'usMoney',
dataIndex: 'price'
}, {
text: 'Change',
width: 75,
sortable: true,
dataIndex: 'change',
renderer: specialRender,
width: 200
}, {
text: '% Change',
width: 75,
sortable: true,
dataIndex: 'pctChange'
}, {
text: 'Last Updated',
width: 85,
sortable: true,
renderer: Ext.util.Format.dateRenderer('m/d/Y'),
dataIndex: 'lastUpdated'
}],
height: 350,
width: 600,
title: 'Irm Grid Example',
renderTo: 'grid-example',
viewConfig: {
stripeRows: true
}
});
/**
* when the grid view is ready this method will find slider columns and render the slider to them
*/
onGridViewReady = function() {
var recordIdx,
colVal,
colEl;
for (recordIdx = 0; recordIdx < grid.store.getCount(); recordIdx++) {
record = grid.store.getAt(recordIdx);
sliderHolder = Ext.DomQuery.select('.slider-target', grid.view.getNode(recordIdx));
if (sliderHolder.length) {
colEl = sliderHolder[0];
// remove div generated by grid template - alternative is to use a new template in the col
colEl.innerHTML = '';
// get the value to be used in the slider from the record and column
colVal = record.get('change');
// render the slider - pass in the full record in case record data may be needed by change handlers
renderNumericFilter(colEl, colVal, record)
}
}
}
// when the grids view is ready, render sliders to it
grid.on('viewready', onGridViewReady, this);
// modification of existing method but removed from custom column
renderNumericFilter = function(el, val, record) {
var filterPanel = Ext.widget('slider', {
width: 200,
value: val,
record: record,
minValue: 0,
maxValue: 10,
renderTo: el
});
}
});
I did something like this when I needed to render a small chart (essentially a spark chart) in a grid column. This solution is similar to sha's, but it's more robust and delegates the rendering to the component being rendered rather than the Column, which doesn't really have a render chain.
First, the column class:
Ext.define("MyApp.view.Column", {
extend: "Ext.grid.column.Column",
// ...
renderer: function (value, p, record) {
var container_id = Ext.id(),
container = '<div id="' + container_id + '"></div>';
Ext.create("MyApp.view.Chart", {
type: "column",
// ...
delayedRenderTo: container_id
});
return container;
}
});
Note the delayedRenderTo config option. Just like renderTo, this will be the DOM ID of the element that the chart component will render to, except that it doesn't need to be present in the DOM at the time of creation.
Then the component class:
Ext.define("MyApp.view.Chart", {
extend: "Ext.chart.Chart",
// ...
initComponent: function () {
if (this.delayedRenderTo) {
this.delayRender();
}
this.callParent();
},
delayRender: function () {
Ext.TaskManager.start({
scope: this,
interval: 100,
run: function () {
var container = Ext.fly(this.delayedRenderTo);
if (container) {
this.render(container);
return false;
} else {
return true;
}
}
});
}
});
So during initComponent(), we check for delayed render and prepare that if necessary. Otherwise, it renders as normal.
The delayRender() function itself schedules a task to check every so often (100ms in this case) for the existence of an element with the given ID — i.e., to check whether the column has rendered. If not, returns true to reschedule the task. If so, renders the component and returns false to cancel the task.
We've had good luck with this in the field, so I hope it works for you too.
By the way, I was developing this as a part of answering my own question about ExtJS charting. That thread has the results of my performance testing. I was rendering 168 chart components in grid columns in 3-4s across most browsers and OSes. I imagine your sliders would render much faster than that.
Try something like this:
renderNumericFilter: function () {
var id = Ext.id();
Ext.defer(function () {
Ext.widget('slider', {
renderTo: id,
width: 200,
value: 50,
increment: 10,
minValue: 0,
maxValue: 100,
});
}, 50);
return Ext.String.format('<div id="{0}"></div>', id);
}
But I must say whatever you're trying to do - it doesn't sound right :) I don't think a bunch of sliders inside the grid will look good to the user.

Cannot create new javascript object

I am trying to use the StoreSeries with Dojo in order to create charts. However when I try to create the array by:
new StoreSeries(store, { query: { site: 1 } }, "value");
Then the javascript stops running and cannot continue to render the chart.
This is all the script that I think might be relevant - ask if you need to see any more.
function setupWeekElectricBar(Chart, theme, ClusteredColumns, Columns, Tooltip, Highlight, Observable, Memory, StoreSeries)
{
var data = [
{ id: 1, value: 5, site: 1 },
{ id: 2, value: 2, site: 1 },
{ id: 3, value: 3, site: 1 },
{ id: 4, value: 1, site: 1 },
{ id: 5, value: 3, site: 1 },
{ id: 6, value: 1, site: 1 }
];
// Create the data store
// Store information in a data store on the client side
var store = Observable(new Memory({
data: {
identifier: "id",
label: "Users Online",
items: data
}
}));
var result = new StoreSeries(store, { query: { site: 1 } }, "value");
//function does not get past here (checked using alert())
}
require([
// Require the basic chart class
"dojox/charting/Chart",
// Require the theme of our choosing
"dojox/charting/themes/Tufte",
// Charting plugins:
// We want to plot Pie and ClusteredColumns charts
"dojox/charting/plot2d/Pie",
"dojox/charting/plot2d/ClusteredColumns",
"dojox/charting/plot2d/Columns",
"dojox/charting/plot2d/Grid",
// Retrieve the Legend, Tooltip, and MoveSlice classes
"dojox/charting/action2d/Tooltip",
"dojox/charting/action2d/MoveSlice",
"dojox/charting/action2d/Highlight",
// We want to use Markers
"dojox/charting/plot2d/Markers",
// We'll use default x/y axes
"dojox/charting/axis2d/Default",
"dojo/parser",
"dojo/store/Observable",
"dojo/store/Memory",
"dojox/charting/StoreSeries",
"dijit/dijit", // loads the optimized dijit layer
"dijit/Calendar",
// Wait until the DOM is ready
"dojo/domReady!"
], function(Chart, theme, Pie, ClusteredColumns, Columns, Grid, Tooltip, MoveSlice, Highlight, Observable, Memory, StoreSeries) {
setupWeekElectricBar(Chart, theme, ClusteredColumns, Columns,Tooltip, Highlight, Observable, Memory, StoreSeries);
}
});
Fixed by moving
"dojo/store/Observable",
"dojo/store/Memory",
"dojox/charting/StoreSeries",
further up.

Categories