Highstock setExtremes with a custom range selector button - javascript

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())

Related

Getting values instead of percentages of dataZoom in Apache Echarts

I am using Apache Echarts, and I have a chart with an X axis of type time, with a dataZoom of type slider. This serves as an example:
var values = [];
for(var i = 0; i < 15; i++) {
var date = new Date();
date.setDate(date.getDate() + i);
values.push([date, i])
}
const firstDate = values[1][0];
const lastDate = values[5][0];
option = {
xAxis: {
type: 'time',
boundaryGap: false,
},
yAxis: {
type: 'value',
},
dataZoom: [{
type: 'slider',
}],
series: [
{
type: 'line',
data: values,
}
]
};
myChart.on('datazoom', (event) => {
console.log(event)
})
In the event I receive 'start' and 'end' as percentages, but what I would like to receive are the actual start and end date. This means, the values of the axis itself.
I checked the docs, and they say that I can actually get the values when "zoom event of triggered by toolbar". I am not sure whether this refers to the toolbox datazoom (which does not fulfill my needs) or any other thing.
Any help would be appreciated.
As F.Marks answered here, you should get the values in dataZoom echart`s option property object like the code below:
myChart.on('dataZoom', function() {
var option = myChart.getOption();
console.log(option.dataZoom[0].startValue, option.dataZoom[0].endValue);
});

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

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

How to restrict max value on html5 number input on manual entry

Free jqgrid column is defined to use html5 number input type like
{ name: "amount", width: 62, template: "number",
formatter: "number", formatoptions: {decimalSeparator:",", thousandsSeparator: " ", decimalPlaces: 4, defaultValue: '0.0000'},
editoptions: {
maxlength; 4
type: "number",
max: 9999
} },
It allows to enter numbers from keyboard greater than 9999.
max: 9999 affects only to entry using spinner.
How to fix this so that keyboard entry cannot exceed 9999 ?
testcase is at
http://jsfiddle.net/jhckz7rr/3/
It allows to manually enter numbers greater that 9999 into Amount column. How to restrict manual entry to 9999 ?
I tried also to use string max value max: '9999' but problem persists.
If input type is text, input respects maxlength value.
Try to use something like the following
{
name: "amount",
width: 62,
template: "number", // formatter: "number"
formatoptions: {
decimalSeparator: ",",
thousandsSeparator: " ",
decimalPlaces: 2,
defaultValue: "0,00"
},
editoptions: {
maxlength: 7,
type: "number",
max: "9999",
dataEvents: [
{
type: "blur",
fn: function (e) {
if (e.target.checkValidity()) {
$(e.target).removeClass("ui-state-error");
} else {
$(e.target).addClass("ui-state-error");
alert(e.target.validationMessage);
$(e.target).focus();
}
}
}
]
}
}
The above code calls checkValidity() method of <input type="number">. Of cause you need to include additional tests in the code like validation that e.target.checkValidity is a function (for the case of executing in old web browser) and some other. The above code just shows the main idea of validation which uses functionality of <input type="number">.
See the demo http://jsfiddle.net/OlegKi/jhckz7rr/8/, which works for both inline editing and form editing.
Create validation on input with jQuery.
Event listeners are attached on edit click, and removed on save click.
I used setTimeout, to be synchronized with free-jqgrid elements manipulation - as the proper solution will be to extend free-jqgrid functionality
function restrictMax(){
var max = parseFloat($(this).attr('max'))
var value = parseFloat($(this).val())
if($(this).val() > max){
$(this).val(max)
}
}
setTimeout(function(){
$('.fa-pencil').click(function(){ //click on edit
var thatParent = $(this).closest('tr')
setTimeout(function(){
thatParent.find('input[type="number"]').on('input',restrictMax)
},0);
})
$('.fa-floppy-o').click(function(){ //click on save
var thatParent = $(this).closest('tr')
setTimeout(function(){
thatParent.find('input[type="number"]').off('input',restrictMax)
},0)
})
},0)
http://jsfiddle.net/jhckz7rr/6/

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

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.

Categories