Unable to hold onto variable after fetching JSON data - javascript

I am using the highstocks charting library and JQuery for this question. I am attempting to create one chart that is partitioned into three pieces, each with a different set of data. To read in the data, I am using an example from the highstock site, with the following code:
var seriesOptions = [],
names = ['MSFT', 'AAPL', 'GOOG'];
$.each(names, function(i, name) {
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=' + name.toLowerCase() + '-c.json&callback=?', function(data) {
seriesOptions[i] = {
name: name,
data: data
};
});
});
After this code is processed, I use the seriesOptions variable as the series value for each of the three charts as such:
$('#container').highcharts('StockChart', {
// misc options
series: [
seriesOptions[0],
seriesOptions[1],
seriesOptions[2],
]
}
However, it seems that the seriesOptions variable is null after it comes out of the $.getJSON() method call. How can I get around this, and what is happening to the seriesOptions variable after the $.getJSON() call?
Thanks
EDIT: Specific error:
Uncaught TypeError: Cannot read property 'type' of undefined. I am pretty sure that this is referring to the seriesOptions variable, but I'll include it for clarity.

You're probably executing the highCharts call before the AJAX call has completed. You're also doing an AJAX call in a loop (albeit a small loop, it can still have the same general issues) - if you can - try and make this 1 AJAX call with all the params. If that's not an option - you can loop, and verify all the calls are done, then process:
var seriesOptions = [],
names = ['MSFT', 'AAPL', 'GOOG'];
var completedCalls = 0;
for (var i = 0; i < names.length; i++) {
var name = names[i];
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=' + name.toLowerCase() + '-c.json&callback=?', function(data) {
seriesOptions.push({
name: name,
data: data
});
completedCalls++;
if (completedCalls == names.length) {
//All ajax calls done, do highcharts magic!
$('#container').highcharts('StockChart', {
// misc options
series: [
seriesOptions[0],
seriesOptions[1],
seriesOptions[2],
]
})
}
});
}

jQuery provides an interface using $.when for resolving multiple ajax calls using deferred/promise objects. An example using your configuration can be seen below.
var seriesOptions = [];
$.when(
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=msft-c.json&callback=?'),
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=aapl-c.json&callback=?'),
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=goog-c.json&callback=?')
).done(function (r1, r2, r3) {
seriesOptions.push({
name: "MSFT",
data: r1
});
seriesOptions.push({
name: "AAPL",
data: r2
});
seriesOptions.push({
name: "GOOG",
data: r3
});
$('#container').highcharts('StockChart', {
// misc options
series: [
seriesOptions[0],
seriesOptions[1],
seriesOptions[2],
]
});
});

Related

IIFE View Model seems to be undefined

I am using Mithril.JS and it looks like my vm is undefined where as prior it wasn't.
I searched around and there is very little out there in terms of mithril.js.
Code:
var app = {};
var apiData;
app.getData = function () {
m.request({
method: 'GET',
url: '/api/stocks',
}).then(function(data){
data = apiData;
})
};
app.App = function(data){ // model class
this.plotCfg = {
chart: {
renderTo: "plot"
},
rangeSelector: {
selected: 4
},
yAxis: {
labels: {
formatter: function () {
return (this.value > 0 ? ' + ' : '') + this.value + '%';
}
},
plotLines: [{
value: 0,
width: 2,
color: 'silver'
}]
},
plotOptions: {
series: {
compare: 'percent',
showInNavigator: true
}
},
tooltip: {
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
valueDecimals: 2,
split: true
},
series: [{
name: 'Kyle\'s Chart',
data: apiData
}]
};
};
app.controller = function() { // controller
this.apk = new app.App();
this.cfg = this.apk.plotCfg;
};
app.plotter = function(ctrl) { // config class
return function(elem,isin) {
if(!isin) {
m.startComputation();
var chart = Highcharts.StockChart(ctrl.cfg);
m.endComputation();
}
};
};
app.view = function(ctrl) { // view
return m("#plot[style=height:400px]", {config: app.plotter(ctrl)})
};
app.Stock = function(data) {
this.date_added = m.prop(new Date());
this.symbol = m.prop(data.symbol);
this.id = m.prop(data.id)
};
app.SymbolList = Array;
app.vm = (function() {
var vm = {}
vm.init = function() {
//a running list of todos
vm.list = new app.SymbolList();
//a slot to store the name of a new todo before it is created
app.parseData = function (data) {
for (var i =0; i< list.length ;i++) {
console.log(list[i].stock);
var stockSymbol = data[i].stock;
vm.list.push(new app.Stock({symbol : stockSymbol}));
}
app.parseData(apiData);
vm.symbol = m.prop("");
//adds a todo to the list, and clears the description field for user convenience
vm.add = function() {
var data = vm.symbol();
if (vm.symbol()) {
data = {'text': data.toUpperCase()};
m.request({method: 'POST',
url: '/api/stocks',
data: data,
}).then(function(list) {
vm.list = [];
for (var i =0; i< list.length ;i++) {
console.log(list[i].stock);
var stockSymbol = list[i].stock;
vm.list.push(new app.Stock({symbol : stockSymbol}));
}
return;
})
vm.symbol("");
}
};
}
return vm
}
}())
app.controller2 = function() {
app.vm.init();
}
app.view2 = function() {
return [
m('input', { onchange: m.withAttr('value', app.vm.symbol), value: app.vm.symbol()}),
m('button.btn.btn-active.btn-primary', {onclick: app.vm.add}, 'Add Stock'),
m('ul', [
app.vm.list.map(function(item , index) {
return m("li", [
m('p', item.symbol())
])
})
])
]
};
m.mount(document.getElementById('chart'), {controller: app.controller, view: app.view}); //mount chart
m.mount(document.getElementById('app'), {controller: app.controller2, view: app.view2}); //mount list
<div id="app"></div>
<div id="chart"></div>
<script src="https://cdn.rawgit.com/lhorie/mithril.js/v0.2.5/mithril.js"></script>
<script src="https://code.highcharts.com/stock/highstock.js"></script>
The error that pops up in chrome is this:
app.js:119 Uncaught TypeError: Cannot read property 'init' of undefined
at new app.controllerT (app.js:119)
at ea (mithril.js:1408)
at Function.k.mount.k.module (mithril.js:1462)
at app.js:135
It was fine before I added the second mount-point, view and controller.
Any ideas?
The problem is that app.vm doesn't expose init.
The current code for app.vm looks like this:
app.vm = (function(){
var vm = {}
vm.init = function(){
/* lots of stuff... */
return vm
}
}())
This means the internal vm.init returns vm, but the app.vm IIFE doesn't return anything. It should be:
app.vm = (function(){
var vm = {}
vm.init = function(){
/* lots of stuff... */
}
return vm
}())
It's very difficult to read your application structure because it's full of a variety of exotic patterns that don't seem to be useful. Admittedly, the 'vm' closure is a pattern introduced in Mithril guides, but I think it's far easier to write, reason about and debug applications if we avoid all these closures, initialisation calls, constructors, nested objects and namespaces.
The idea behind 'view models' comes from the state of web app development when Mithril was originally released (early 2014), when one of the principle concerns in front-end app development was a perceived lack of structure, and Mithril felt it necessary to show people how to structure objects. But structure of this form is only useful if it clarifies intent - in the code above it confuses things. For example, app.getData isn't called anywhere, and it assigns an empty global variable to its own argument before disposing of it. This kind of thing would be easier to reason about if we had less objects.
Here's the same code with some extra fixes and an alternate structure. The principles at work in this refactor:
We're no longer writing any of our own constructors or closures, resulting in less dynamic code execution and avoiding potential for errors like app.vm.init
We're no longer attaching things to objects unless that structure is useful or meaningful, and using simple variables or declaring things at the point of use if they're only used once, resulting in less references and less structural complexity
We use object literals - var x = { y : 'z' } instead of var x = {}; x.y = 'z' so we can see holistic structures rather than having to mentally interpret code execution to work out how objects will be built at runtime
Instead of using one big generic app.vm to store everything, we separate our app model into the places where they are relevant and use functions to pass values from one place to another, allowing us to split our complexity. I'll elaborate on this after showing the code:
// Model data
var seriesData = []
// Model functions
function addToSeries(data){
seriesData.push.apply(seriesData,data)
}
function getData( symbol ){
m.request( {method: 'POST',
url: '/api/stocks',
data: { text : symbol.toUpperCase() },
} ).then(function(list) {
return list.map(function( item ){
return makeStock( { symbol : item.stock } )
} )
} )
}
function makeStock( data ) {
return {
date_added : new Date(),
symbol : data.symbol,
id : data.id
}
}
// View data
var chartConfig = {
rangeSelector: {
selected: 4
},
yAxis: {
labels: {
formatter: function () {
return (this.value > 0 ? ' + ' : '') + this.value + '%';
}
},
plotLines: [{
value: 0,
width: 2,
color: 'silver'
}]
},
plotOptions: {
series: {
compare: 'percent',
showInNavigator: true
}
},
tooltip: {
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
valueDecimals: 2,
split: true
},
series: [{
name: 'Kyle\'s Chart',
data: seriesData
}]
}
// Components
var chartComponent = {
view : function(ctrl) {
return m("#plot[style=height:400px]", {
config: function(elem,isin) {
if(!isin)
Highcharts.StockChart(elem, chartConfig)
}
})
}
}
var todosComponent = {
controller : function(){
return {
symbol : m.prop('')
}
},
view : function( ctrl ){
return [
m('input', {
onchange: m.withAttr('value', ctrl.symbol),
value: ctrl.symbol()
}),
m('button.btn.btn-active.btn-primary', {
onclick: function(){
if( ctrl.symbol() )
getData( ctrl.symbol() )
.then( function( data ){
addToSeries( data )
} )
ctrl.symbol('')
}
}, 'Add Stock'),
m('ul',
todos.map(function(item) {
return m("li",
m('p', item.symbol)
)
})
)
]
}
}
// UI initialisation
m.mount(document.getElementById('chart'), chartComponent)
m.mount(document.getElementById('app'), todosComponent)
There is no more app, or vm, or list. These end up being unhelpful because they're so vague and generic they get used to store everything - and when one object contains everything, you may as well have those things freely available.
The core dynamic data list is now called seriesData. It's just an array. In order to interact with it, we have 3 simple functions for mutating the series data, fetching new data, and creating a new data point from input. There's no need for constructors here, and no need for props - props are a Mithril utility for being able to conveniently read and write data from an input - they're incompatible with the Highcharts API in any case.
That's all the model data we need. Next we have the code specific to our UI. The Highcharts config object references seriesData, but apart from that its an esoteric object written to conform with Highcharts API. We leave out renderTo, because that's determined dynamically by our Mithril UI.
Next comes the components, which we write as object literals instead of piecing them together later - a component controller only makes sense in relation to its view. The chartComponent doesn't actually need a controller, since it has no state and just reads previously defined model and view data. We supply the element reference directly to the Highcharts API in the config function. Because this function is only used once in a single place, we declare it inline instead of defining it in one place and binding it somewhere else. start/endComputation are unnecessary since the process is synchronous and there's no need to stop Mithril rendering during this process.
I couldn't quite work out how the 'todos' model was meant to work, but I assumed that the second component is designed to provide an alternate view of data points and allow user input to define and fetch more data. We store the 'symbol' prop in the controller here, since it's a stateful property that's used exclusively by the view. It's our only stateful property relating to this component, so that's all we define in the controller. Earlier on we simplified the model-related functions - now in the view we interact with these, passing in the symbol data explicitly instead of defining it elsewhere and retrieving it in another place. We also reset the value here, since that's an aspect of this component's logic, not the overal data model.

Add Categories to Highcharts via Loop

I'm trying to add a list of categories as the xAxis for my bar chart using JavaScript and a JSON object. I've thus far had no success as the following code produces the following along the xAxis:
[object Object]
1
2
The JSON object I'm receiving is like this:
0: {Item: "textforyou", Num: 88}
1: {Item: "MoreText", Num: 22}
2: {Item: "SimpleStuff", Num: 85}
I'd like the have the categories on the xAxis look more like this:
textforyou
MoreText
SimpleStuff
This bit of code is where I'm taking my JSON object and trying to get the categories and series data for it.
$.ajax({
xhrFields: { withCredentials: true },
url: areaUrl + "api/Problem/ProblemsYTD",
success: data => {
self.isLoading(false);
self.data(data);
self.setPlotData(data);
self.isLoaded(true);
},
error: data => {
self.loadingError(true);
}
});
}
self.setPlotData = (data: any) => {
var len = data.List.length,
i;
var dataCats = new Array();
for (i = 0; i < len; i++) {
dataCats.push(
{ name: data.List[i].Service }
)
}
self.plotDataLabels.push({ data: dataCats });
var dataItems = [];
for (i = 0; i < len; i++) {
dataItems.push(
{ y: data.List[i].DaysOpen }
)
}
self.plotData.push({ data: dataItems });
}
Lastly, on the page where the bar chart is being created, this is the command I have for the xAxis category section of the page.
xAxis: {
categories: viewModel.plotDataLabels(),
crosshair: false
},
I'm able to do this fine for the series data, but I can't seem to get it right for the categories/text. Any help would be appreciated.
EDIT: Per Request, when I console.log ViewModel.plotDataLabels(), I get a single object back with "data: Array[3]". Once I access that array, everything appears as the following
Dropdown 0: Object
Dropdown 1: Object
Dropdown 2: Object
name: "SimpleStuff"
__proto__: Object
You need to manipulate your ViewModel.plotDatalabels() to ensure you get just the name and not the whole object. This should work, replace your categories value with the following
categories: viewModel.plotDataLabels().map(function(obj){ return obj.name; })

How to recreate a table with jQuery DataTables

I'm essentially using the top answer here ( from sdespont) to try to destroy some tables.
I have one table that shows me the status of a .csv file being uploaded.
FileuploadTable:
FileName FileType FileSize AvailableActions
I have a second table that is the table displaying data from the .csv file.
I need provide the user the ability to reset the form, i.e. get rid of the .csv, and get rid of all of the data, destroy() the two tables separately, and empty() them of all the data that was there initially.
Here is the issue I'm running into.
I can't seem to set the column titles of FileUploadTable after destroy() and empty(). When I attempt to upload a new file, the elements are still on the page, just empty, though the same initialization is being called
I can't seem to get rid of the column titles in CSVTable after destroy() and empty(). When I attempt to upload a different csv, it tries to match column headers to the ones that should have been destroyed, but they don't match because, though CSVTable was destroyed and emptied, the column titles are still there...?
Not sure what I'm missing. They're being set properly on initial create.
$(elem).DataTable()
Can anyone show me a basic working implementation of destroying/emptying datatables, then re initializing with different data, so I can try to mimic it. My brain is mush from looking at their docs for the last 3 days, making no progress.
Example of my data object
[
{
//key = column title
//"val" = data in row
//object = row
key: "val",
//i.e.
FirstName: "Bob",
LastName: "Barker",
Age: 800,
//etc
},
//etc
]
OK. You can make a simple iteration over your data using Object.keys() that produces a column object on the fly holding corresponding data and title values :
var columns = [], keys = Object.keys(data[0]);
for (var i=0;i<keys.length;i++) {
columns.push({ data: keys[i], title: keys[i] });
}
Use that inside a general function that initialises the table and take care of destroying and emptying if already initialized :
var table = null;
function initTable(data) {
var columns = [], keys = Object.keys(data[0]);
for (var i=0;i<keys.length;i++) {
columns.push({ data: keys[i], title: keys[i] });
}
if (table) {
table.destroy();
$('#example').empty();
}
table = $('#example').DataTable({
data: data,
columns : columns
})
}
Now imagine the following is the success handlers of your AJAX calls, or however you get the new data that should be populated to the table :
$('#insert1').on('click', function() {
var data = [
{ FirstName: "Bob", LastName: "Barker", Age: 800 },
{ FirstName: "John", LastName: "Doe", Age: 'N/A' }
]
initTable(data);
})
$('#insert2').on('click', function() {
var data = [
{ Animal : "Lion", Taxon : 'Panthera leo' },
{ Animal : "Cheetah", Taxon : 'Acinonyx jubatus' }
]
initTable(data);
})
demo -> http://jsfiddle.net/d5pb3kto/

$scope.$watch ... make it return a function on another scope?

I have a watch function that returns a value for 2 series of data in Highcharts. The example below shows how this would work within the controller:
$scope.$watch("trendCompany",function(){
return $scope.chartConfig.series[0].data =
[-1.95,0.99,9.29,4.49,-1.50,-4.05,-7.05,10.13,-19.95,
2.99,-14.55,-6.15,-20.25,-27.00,-26.10,-10.95,-10.95,5.30,
6.06,11.12,10.13,11.96,22.13,6.74,4.67,1.43,0.27,4.20,-3.45,
2.10,-1.65,1.92,1.85,0.00,1.97,-5.25,-3.30,1.67,3.87,6.27,1.89,
2.27,0.59,-1.20,-5.85,-6.60,-2.25,-2.40,-2.85,-3.45,-0.15,2.63],
$scope.chartConfig.series[1].data =
[1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2];
});
$scope.chartConfig = {
options: {
chart: {
type: 'line'
}
},
series: [{
data: []
},{
data: []
}],
title: {
text: 'Hello'
},
loading: false
}
My question is.. how would
$scope.chartConfig.series[i].data be equal to another function rather than this static array? I have a function that returns a result of data like so:
//Let a propertyExpr string define a traversal route into an object
$scope.dataResult = function(obj, propertyExpr) {
return $parse(propertyExpr)(obj);
}
My first attempt was to go back to the watch function and do...
$scope.chartConfig.series[i].data = dataResult(obj, property);
Then, I tried...
$scope.chartConfig.series[i].data = function(){ return dataResult(obj,property); }
However, neither of these are working. How can I make the value of the series scope equal to something that is invoked by a function?
If you define a function on the scope you also have to call it like that.
$scope.chartConfig.series[0].data = $scope.dataResult($scope.obj, $scope.property);

select2: "text is undefined" when getting json using ajax

I'm having an issue when getting json results back to select2. My json does not return a result that has a "text" field so need to format the result so that select2 accepts "Name".
This code works if the text field in the json is set to "text" but in this case, I cannot change the formatting of the json result (code outside my control).
$("#e1").select2({
formatNoMatches: function(term) {return term +" does not match any items." },
ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
url: "localhost:1111/Items.json",
dataType: 'jsonp',
cache: true,
quietMillis: 200,
data: function (term, page) {
return {
q: term, // search term
p: page,
s: 15
};
},
results: function (data, page) { // parse the results into the format expected by Select2.
var numPages = Math.ceil(data.total / 15);
return {results: data.Data, numPages: numPages};
}
}
});
I have looked into the documentation and found some statements you can put into the results such as
text: 'Name',
but I am still getting "text is undefined".
Thanks for any help.
note that select2 is always in {id,text} pair so you need to specify both
results: function (data, page) {
var newData = [];
_.each(data, function (item) {
newData.push({
id: item.Id //id part present in data
, text: item.DisplayString //string to be displayed
});
});
return { results: newData };
}
},
Thanks to #neel shah for solving my problem. i had just little problem, i didnt wanted to use extra library so thats why i changed to normal jquery.
so if wanna go for normal jquery or javascript.
results: function (data, page) {
var newData = [];
$.each(data, function (index,value) {
newData.push({
id: value.Id, //id part present in data
text: value.DisplayString //string to be displayed
});
});
}
OR
results: function (data, page) {
var newData = [];
for ( var i = 0; i < data.length; i++ ) {
newData.push({
id: data[i].Id, //id part present in data
text: data[i].DisplayString //string to be displayed
});
}
All credits go to neel shah. Thanks again.

Categories

Resources