I have a simple issue relating to function sequence that I'm struggling to solve on my own. Essentially, I'm rendering a bar chart based on parameters selected by the user. This works perfectly on the first go, but when the user updates their parameter selection and hits "render" again, the chart doesn't update. It's only on the second button click that the chart renders with updated data.
I can see in the console that the "generate chart" function IS receiving the updated data, so I know the issue is that the chart is rendering before the new data is loaded. I understand that the common solution to this type of problem would be using JS Promises or callbacks, but I can't figure out how to execute it, and would really appreciate some help.
Here's the code structure. I have to make two API calls to get the data.
const ajaxCallOne = () => {
// Make API call for first data set (data1). On success, invoke the next API call, passing data1.
success: (data1) => {
ajaxCallTwo(data1);
}
}
const ajaxCallTwo = (data1) => {
// Make second API call for another set of data ('data2'). On success, invoke a function to visualize all the data in a bar chart, passing it both data1 and data2.
success: (data2) => {
populateBarChart(data1, data2)
}
}
const populateBarChart = (data1, data2) => {
// Function to create a bar chart and populate it with data1 and data2 from the API calls
// The data retrieved from the API calls is based on parameters selected by the user. When the user selects new parameters and hits the "view results" button again, we kick off another round of API calls, which pass the new data to this populateBarChart function.
// I am using ApexCharts.js library to render the bar chart. The syntax for actually rendering the chart looks like this:
let chart = new ApexCharts(
document.querySelector("#audience-reach-chart-container"),
options
);
chart.render();
}
At first I thought I needed to destroy / empty out the chart at the beginning of the function, but since the function creates a new instance of the chart every time it's invoked, there's nothing to target and destroy.
Also, when I console.log the data being received by the populateBarChart function, I can see the updated data is being received. So the issue must be that the chart is rendering based on the old data, and I need to control the sequence of events?
I can't figure out how to go about this within the code design. Any insights would be greatly appreciated.
You're absolutely right that this can be solved using callbacks, promises and even async/await.
However, this problem statement may have a nuance e.g. if you're using <select> dropdowns, the event listener registered on them could be slightly incorrect. OR, if the ApexCharts library's behavior is to withhold rendering for some reason, then it'll be hard to say so without going through the documentation. Hence, an example on codesandbox or jsfiddle can help us.
Let's look at one possible solution - Ideally, when using promises, we should do something like this:
let container = document.querySelector("#audience-reach-chart-container");
const ajaxCallOne = () => {
// Make API call for first data set (data1).
// No need to invoke the next API call. Just ensure it returns a promise object
}
const ajaxCallTwo = () => {
// Make second API call for another set of data ('data2').
// Sit still! No need to do anything here as long as this function returns a promise
}
// Run this when the user hits 'render' button
Promise.all([ajaxCallOne, ajaxCallTwo]).then((values) => {
console.log(values);
// values[0] holds data1
// values[1] holds data2
let options = doSomethingOnThese(values[0], values[1]);
// Populate the BarChart here
let chart = new ApexCharts(container, options);
chart.render();
});
I've created a sample app(https://codesandbox.io/s/unruffled-cookies-tbcerp?file=/src/index.js) to mimic your usecase here:
// https://codesandbox.io/s/unruffled-cookies-tbcerp?file=/src/index.js
(async function () {
var options = {
chart: {
height: 350,
type: "bar"
},
dataLabels: {
enabled: false
},
series: [],
title: {
text: "Ajax Example"
},
noData: {
text: "Loading..."
}
};
var chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
try {
const ajaxCallOne = await fetch(
"https://my-json-server.typicode.com/apexcharts/apexcharts.js/yearly"
);
const ajaxCallTwo = await fetch(
"https://my-json-server.typicode.com/apexcharts/apexcharts.js/yearly2"
);
let data1 = await ajaxCallOne.json();
let data2 = await ajaxCallTwo.json();
console.log(data1, data2);
chart.updateSeries([
{
name: "Sales",
data: data1
},
{
name: "Sales2",
data: data2
}
]);
} catch (e) {
console.log(e);
}
})();
Related
Im in javascript on a HTML page trying to use Solr query data in a table.
So what I want to do is get a bunch of different responses to solr queries, and store them in an array as they come in. Then return each of these results to their relevant spot in a graphic chart to display my information.
What I am having trouble doing is storing the result from JSON in a variable for later use. However it won't save, as the results show up as undefined when checked. Yet if I assign the response to some HTML location like with $('#yoyo') from within function on_data(data), it seems to work just fine.
So I can get results live, but I can't store them for some reason?
From reading lots of other posts it seems it could be something to do with it being asynchronous. It feels to me like functions are running out of time. Like its trying to return the answer before it's assigned any value or something.
Can somebody show me where I'm going wrong here?
if (typeof variable !== 'undefined') {
var global_tick = -1
}
var results = []
var testB = 'query response failed';
$('#testB').prepend('<div>' + testB + '</div>');
function on_data(data) {
results[global_tick] = parseInt(data.response.numFound);
$('#yoyo').prepend('<div>' + results[global_tick] + '</div>');
global_tick = global_tick + 1;
}
function partyQuery(party, region){
pQuery(party, region)
var res = pResult()
return res
}
function pQuery(party, region){
var url='http://localhost:8983/solr/articles/select?q=text:'+party+'+AND+region:'+region+'&wt=json&callback=?&json.wrf=on_data';
$.getJSON(url);
}
function pResult(){
return results[global_tick]
}
//Parties
var pqFG = partyQuery("\"Fine Gael\"", 'ROI');
var pqFF = partyQuery("\"Fianna Fail\"", 'ROI');
// Load the Visualization API and the corechart package.
google.charts.load('current', {'packages':['corechart']});
// Set a callback to run when the Google Visualization API is loaded.
google.charts.setOnLoadCallback(drawChart);
// Callback that creates and populates a data table,
// instantiates the pie chart, passes in the data and
// draws it.
function drawChart() {
// Party data table.
var data_party = new google.visualization.DataTable();
data_party.addColumn('string', 'Party');
data_party.addColumn('number', 'Mentions');
data_party.addRows([
['Fine Gael', pqFG],
['Fianna Fáil', pqFF],
]);
I have written a following code in my .NET Controller action which fetches specific time zone. After that I Group by the data based on that time zone like following:
[HttpPost]
[ActionName("FetchTimeZone")]
public ActionResult FetchTimeZone(string timeZone)
{
using (var ctx = new myContext())
{
var loggedUser = ctx.Users.FirstOrDefault(x => x.Email == User.Identity.Name);
var userStores = ctx.UserStores.Where(x => x.UserId == loggedUser.UserId).SelectMany(x => x.StoreItems.SelectMany(y => y.StoreItemTransactions)).ToList();
var hourlyData = userStores
.GroupBy(x => TimeZoneInfo.ConvertTime(x.TransactionDate.Value, TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time")).Hour)
.Select(pr => new HourlyGraph { Hour = pr.Key, Sales = pr.Sum(x => x.QuantitySoldTransaction) })
.ToList();
hourlyData.AddRange(Enumerable.Range(0, 24)
.Except(hourlyData.Select(m => m.Hour))
.Select(i => new HourlyGraph() { Hour = i, Sales = 0 }));
ViewBag.HourlyGraph = hourlyData.OrderBy(x => x.Hour).ToList();
return Json("OK");
}
}
All here is good, so we move on to the View part with jQuery and data representation.. Initially when the page is loaded I fill graph with CEST time zone and it represents the data correctly. So I made an on click event which basically should redraw the morris graph based on the time zone that was sent.
By default I set in code Tokyo Standard Time just to get different results to verify whether this works or not. So initially I fill the graph like this:
var hourlyGraph = Morris.Bar({
element: 'graph_bar',
data: [
#foreach (var item in ViewBag.HourlyGraph)
{
#:{device: '#item.Hour.ToString("D2"):00', geekbench:#item.Sales },
}
],
xkey: 'device',
ykeys: ['geekbench'],
labels: ['Sold'],
barRatio: 0.4,
barColors: ['#0A4D70', '#34495E', '#ACADAC', '#3498DB'],
xLabelAngle: 35,
hideHover: 'auto',
resize: true
});
The onclick event looks like this:
$('.convertTimeZone').click(function(event){
event.preventDefault();
$.post("/GeoTargeting/FetchTimeZone",{timeZone: "timeZone"}).done(function(data){
hourlyGraph.redraw();
$(window).trigger('resize');
});
});
This is the part that doesn't works:
hourlyGraph.redraw();
$(window).trigger('resize');
I need to somehow now just redraw the graph after the post is done and display it on my View... But I'm not sure what am I doing wrong here... Can someone help me out ?
Your method is returning json (and the only thing in the response is "OK" - it does not contain any data that could be used to update your chart). There is no point setting ViewBag properties unless you return a view (and the view is using those values).
Your method should either
Return a partial view containing the html for a new graph based on
the values of hourlyData and in the success callbacl of your
$.post() function, update the DOM by replacing the existing graph,
or
Return json containing the data for updating the graph, for example
var data = hourlyData.Select(x => new { device = x.Hour.ToString(...), geekbench = x.Sales }); return Json(data); so
that in the success callback, you can the update the data in the graph (e.g. using hourlyGraph.setData(data);)
this is a kind of embarrassing question but I'm stuck.
My background is managed code and I never learned JavaScript but yet I want to implement a tiny project.
The script is running on SharePoint 2010, queries items from a custom list using the JavaScript Object Model and populates a Google chart or table respectively.
With the help of MSDN and Google Developer I was able to query data from one list and visualize it.
However, I'm unable to transfer the concept to query multiple lists, combine result sets and finally pass to Google API.
In my code I created a chain of callbacks like showChart->loadListData->drawChart. This proves to be bad design since it's inflexible and cannot be extended. All API methods are asynchronous and don't have return values but expect method names to call once finished. This is what get's me stuck and where I lack knowledge.
I'm very happy for every comment and answer, also I can provide actual source code if requested. Thank you in advance, Toby
UPDATE as asked for by #Utkanos:
var listItems;
$(document).ready(function() {
ExecuteOrDelayUntilScriptLoaded(loadChartData, "sp.js");
});
function loadChartData() {
var camlQuery = SP.CamlQuery.createAllItemsQuery();
camlQuery.set_viewXml("<View><Query><Where><Eq><FieldRef Name='Year'/><Value Type='Text'>2015</Value></Eq></Where></Query></View>");
loadListData('CustomList', camlQuery, drawChart, readListItemFailed);
}
function loadListData(listTitle, camlQuery, onSuccess, onFail) {
context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle(listTitle);
var listItems = list.getItems(camlQuery);
context.load(listItems);
context.executeQueryAsync(function(sender, args){onSuccess(listItems);}, onFail);
}
function drawDpOverviewChart(listItems) {
var data;
var enumerator = listItems.getEnumerator();
data = new google.visualization.DataTable();
data.addColumn('string', 'Column1');
data.addColumn('number', 'Column2');
var listItem;
while (enumerator.moveNext()) {
listItem = enumerator.get_current();
data.addRow([listItem.get_item('Title'), Math.round(listItem.get_item('Balance')/10000)/100]);
}
var options = {'title':'Pretty Chart'};
var chart = new google.visualization.PieChart(document.getElementById('chart_div'));
chart.draw(data, options);
}
function readListItemFailed(sender, args) {
alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
if using SP 2010 on a typical .aspx page, you have some tools available,
such as MicrosoftAjax.js and _spPageContextInfo
using the REST API, you can join lists on lookup fields and include fields from both lists in one query
following is an example url for a call to rest...
'/_vti_bin/ListData.svc/MI_Projects?$expand=ModifiedBy&$filter=ModifiedBy/Id eq 738'
this call actually "joins" the list MI_Projects to the UserInformationList by "expanding" ModifiedBy
so when the data returns, you can access any of the user info fields, i.e.
row.ModifiedBy.Name
this can be done with lookup fields on custom lists as well
to make the call, you can use the Sys.Net.WebRequest class from MicrosoftAjax
this class also allows you to pass variables to the callback
see following snippet...
function makeCall() {
// Sys.Net.WebRequest is from MicrosoftAjax.js
var webRequest = new Sys.Net.WebRequest();
webRequest.get_headers()['Cache-Control'] = 'no-cache';
webRequest.get_headers()['Accept'] = 'application/json';
webRequest.get_headers()['Content-Type'] = 'application/json';
webRequest.set_url(_spPageContextInfo.webServerRelativeUrl + '/_vti_bin/ListData.svc/MI_Projects?$expand=ModifiedBy&$filter=ModifiedBy/Id%20eq%20738');
// use the 'user context' to pass variables you want available in the callback
webRequest.set_userContext({
Title: 'variable to pass to completed callback'
});
webRequest.add_completed(restComplete);
webRequest.invoke();
}
// the first argument of callback is the Sys.Net.WebRequestExecutor class
function restComplete(executor, eventArgs) {
if (executor.get_responseAvailable()) {
if (executor.get_statusCode() === 200) {
// get variable passed via user context
var variablePassed = executor.get_webRequest().get_userContext().Title;
// i.e. -- build google table
// add rows received from rest (forEach is from MicrosoftAjax.js)
// list results array = executor.get_object().d.results
Array.forEach(executor.get_object().d.results, function (row) {
data.addRow(row.Title, row.Id, row.ModifiedBy.Name);
}, this);
}
}
}
I'm trying to develop my first Windows 8 Store app (HTML/JS). I am using the Grid App Template which suites my Needs I think the best.
This is my model:
I have three entities: 1. GalleryCategory 2. Gallery 3. GalleryItem.
A Gallery is linked to exactly one Category. A GalleryItem is linked to exactly one Gallery...so nothing fancy here...
I'm using the out of the box data.js file to load all categories and all galleries on the Startup of the app. But when I open the galleryDetail.html (which is supposed to Show all the Images of the particular Gallery) I want to load all Images of the Gallery then. (to avoid to much loading on the beginning).
And now I'm finally coming to the Point that I do not understand:
How can I manage this?? I mean
WinJS.UI.Pages.define("/pages/galleryDetail/galleryDetail.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
var item = options && options.item ? Data.resolveItemReference(options.item) : Data.items.getAt(0);
element.querySelector(".titlearea .pagetitle").textContent = item.group.title;
element.querySelector("article .item-title").textContent = item.title;
element.querySelector("article .item-subtitle").textContent = item.subtitle;
element.querySelector("article .item-image").src = item.backgroundImage;
element.querySelector("article .item-image").alt = item.subtitle;
element.querySelector("article .item-content").innerHTML = item.content;
element.querySelector(".content").focus();
var galleryId = item.key;
WinJS.xhr({ url: "http://someUrlToAnAspNetWebsite/Handlers/GalleryItemsHandler.ashx?galleryId=" + galleryId }).done(
// Complete function
function (response) {
var items = JSON.parse(response.responseText);
items.forEach(function (item) {
galleryItemsList.push(item);
});
dataList = new WinJS.Binding.List(galleryItemsList);
var galleryItemsListView = document.getElementById('galleryItemsListView').winControl;
galleryItemsList.itemDataSource = dataList.dataSource;
},
// Error function
function (response) {
// handle error here...
},
// Progress function
function (response) {
// progress implementation goes here...
}
);
},
my Problem is obivous...the ready function continues / Ends before the data is retrieved...as the async call takes a while.
But I thought using the promise (.done()) will do this for me (synchronising the threads)?? Or do I need to use the join() function. If so, where and how?? Sorry for my issues with this...
Thanks for any help...
The ready function itself is an async function, so you only have to return a promise to tell its caller that its not done until some promise is resolved. So you can fix your issue with 7 key strokes. Just add return before the WinJS.xhr call.
I have a datasource that gets data from the server. It is then used in a datatable. I want to be able to filter the data in the table client-side, without making another call to the server.
// Data source definition
myDataSource = new YAHOO.util.DataSource("myurl");
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
myDataSource.connXhrMode = "queueRequests";
myDataSource.responseSchema = {
resultsList: "ResultSet.Result",
fields: ["field1","field2"]
}
// Datatable definition
myDataTable = new YAHOO.widget.DataTable("container", myColumnDefs,myDataSource, {});
Subclass DataSource and override the sendRequest method so that you call the passed-in callback with your own filtered result set as the results argument.
filterDataSource=function(arg) {
filterDataSource.superclass.constructor.call(this,arg);
}
YAHOO.extend(filterDataSource,YAHOO.util.XHRDataSource);
filterDataSource.prototype.sendRequest=function(request, callback) {
var wrapCallBack=function (request,results,error) {
// !!! do filtering on results here !!!
callback.success.call(this,request,results,error);
};
filterDataSource.superclass.sendRequest.call(this,request, {
success: wrapCallBack, argument: callback.argument
});
}
And make your myDataSource a new filterDataSource instead of a new Yahoo.util.DataSource.
Disclaimer: this code probably doesn't work as written; I ripped it out of some old working code and quite likely skipped over some critical piece. Still, I hope it conveys the basic idea.