I am building a dashboard that graphs some data. Using splunk js charts to display the data.
My problem is not with the charting. But I would suspect my logic.
What is happening:
Client - requests chart data from server
Server - returns this object:
a
Object { splunkResults: Array[3],
metricHTML: "<div id="MetricOne" class="metric"><p class="lead"…s="metric">
<p class="lead">Metric Three</p></div>", metricID: Array[3]}
Client - model.js calls view.insertMetricContainers via callback and inserts object.metricHTML into the correct container
Client - model.js calls view.graphsReady in for loop via callback and creates a chart and inserts it into the correct metric div based on object.metricID[i]
Client - charts are displayed
My problem is that only one of the charts is being displayed, the first one.
If you look at this function in view.js:
splunkjs.UI.ready(chartToken, function() {});
I have a console.log("test"); Which is only printed once.
What is my mistake here?
Let me know if I can provide any more information, I have tried a few approaches and have run out of ideas.
Model.js
dashboard.getSystemMetrics = function(callback) {
var url = "/services/systemMetrics/?";
if (dashboard.url[1]) {
url = url + dashboard.url[1];
}
if (dashboard.limit) {
url = url + "&limit=" + dashboard.limit;
}
$.get(url, function(response) {
callback(response.metricHTML, null);
for (var i = 0; i < response.splunkResults.length; i++) {
console.log(i);
callback(null, response.splunkResults[i], response.metricID[i]);
}
});
};
View.js
view.graphsReady = function (data, id) {
console.log(id);
var chart = null;
var chartToken = splunkjs.UI.loadCharting('/javascripts/splunk.ui.charting.js', function() {
// Once we have the charting code, create a chart and update it.
chart = new splunkjs.UI.Charting.Chart($("#" + id), splunkjs.UI.Charting.ChartType.AREA, false);
});
splunkjs.UI.ready(chartToken, function() {
chart.setData(data, {
"chart.stackMode": "stacked",
"legend.placement": "top",
"axisTitleX.text": "Time",
"axisTitleY.text": "Data"
});
chart.draw();
});
};
Controller.js
$(function(){
// Grab user params
dashboard.url = window.location.href;
dashboard.url = dashboard.url.split("/?");
dashboard.limit = "day";
// Get Service Statuses
//dashboard.getQuickStatus(controller.quickStatusReady);
// TODO listener events
dashboard.getSystemMetrics(controller.systemMetricsReady);
});
controller.systemMetricsReady = function(html, data, id) {
if (html) {
view.insertMetricContainers(html);
} else if (data && id) {
view.graphsReady(data, id);
}
};
If you would like to suggest a better question title, please by all means go ahead.
EDIT - i have checked the logs yes. No errors, I wouldn't ask the question if I had errors.
I did a console.log in the set data function (view.js) and it was only logged once not 3 times like it should have. Although the function graphReady is called 3 times
UPDATE - I have found in the view.js code that this function is only run once. Despite the function view.graphsReady being called 3 times.
var chartToken = splunkjs.UI.loadCharting('/javascripts/splunk.ui.charting.js', function() {
// Once we have the charting code, create a chart and update it.
chart = new splunkjs.UI.Charting.Chart($("#" + id), splunkjs.UI.Charting.ChartType.AREA, false);
console.log(chart);
});
console.log(chartToken);
So, chartToken is printed 3 times. chart however is only printed once.
Related
I've been trying to modify the sample dashboard widget at this location
https://learn.microsoft.com/en-us/vsts/extend/develop/add-dashboard-widget?view=vsts#part-2-hello-world-with-vsts-rest-api
However, reluctantly have to admit I simply can't understand the structure required to extend it
Near the end, it uses "load: function" and returns the outputs of a REST API call, which I can consume however I want
However, I need to make more than one different REST call, and I simply cannot figure out how to get that info usable in my function
I modified the code so it starts like this:
VSS.require(["TFS/Dashboards/WidgetHelpers", "TFS/Work/RestClient","VSS/Service", "TFS/WorkItemTracking/RestClient" ],
I then created a handle for the other call I want to make like this:
var queryClient = VSS_Service.getCollectionClient(TFS_Wit_QueryAPI.WorkItemTrackingHttpClient);
var queryResults = queryClient.getQuery(projectId, "Shared Queries/My Bugs");
However, I cannot consume the contents of queryResults - I know it's working up to a point as if I put in an invalid URL it will error as it knows it can't access anything there. If the URL is correct, no matter what I've tried - even stringify just to see what comes back - I get 'undefined' or something similar (it's definitely a valid JavaScript object)
The key seems to be right at the end when you have "load: function" except that only allows one thing to be returned? The reason I know this is if I change the function that it returns to be the one I've written rather than the one from the sample, it works fine - but the problem remains the same in that I can only process the results of one API call.
You can call more than one APIs, the code in that article is just the simple sample.
For Widget extension, you just need to return the status (e.g. Success()) in load function, so you can return status at the end of the function. For example:
var getQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Feedback")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
var getAnOhterQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Bug")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
return {
load: function (widgetSettings) {
// Set your title
var $title = $('h2.title');
$title.text('Hello World');
var r1= getQueryInfo(widgetSettings);
var r2=getAnOhterQueryInfo(widgetSettings);
if(r1==true && r2==true){
return WidgetHelpers.WidgetStatusHelper.Success();
}else{
return WidgetHelpers.WidgetStatusHelper.Failure("failed, check error in console");
}
}
I'm pretty new to crossfilter and dc, currently trying to build interactive dashboard with them. The data I have would easily go up to around 200MB and my browser would just crash when I do the crossfilter on the client side, so after some search online I decided to run crossfilter on the server and dc for charting on client side, and I'm following the work here dc.js-server-side-crossfilter However, the dc charts are not interacting with each other.
So from what I understand the basic components are, server-side app.js handles Ajax calls sent from the client-side app.js, and perform required filtering then send the dimension and groups in JSON format back to the client-side, then we will create fake dimensions and groups and feed them into dc chart, then each user clicks/drag on the chart will then again create another Ajax call, the whole process repeats again.
Here I post the important parts in somewhat pseudo-code as the original is messy to look at,
Server-side app.js
var app = express();
// define variables that will get passed
var dimensions = {};
var groups = {};
// handle the ajax call coming from client-side
app.get("/refresh", function(req, res, next){
var results = {};
filter = req.query["filter"] ? JSON.parse(req.query["filter"]) : {}
for(group_i in groups){
var group = groups[group_i];
if(filter[group_i]){
dimensions[group_i].filter(filter[group_i]);
}
results[group_i]= {values:group.all(),
top: group.top(1)[0].value,
};
}
// send result back in JSON
res.writeHead(200, { 'content-type': 'application/json' });
res.end((JSON.stringify(results)));
});
load data into memory {
// do crossfiltering
var cf = crossfilter(data);
// dimensions
var dateDim = cf.dimension(function(d) {return d['timestamp'];});
var typeDim = cf.dimension(function(d) {return d['destination_type'];});
// groups
var numByDate = dateDim.group();
var numByType = typeDim.group();
// put these dimensions and groups into corresponding variables
dimensions.dateDim = dateDim;
dimensions.typeDim = typeDim;
groups.numByDate = numByDate;
groups.numByType = numByType;
}
Client side app.js
var timeChart = dc.barChart("#pvs-time-chart");
var filteredData = {};
var queryFilter = {};
var refresh = function(queryFilter){
// make ajax call
d3.json("/refresh?filter="+JSON.stringify(queryFilter), function(d){
filteredData = d;
dc.redrawAll();
});
}
var fakeDateDim = {
filter: function(f){
if(f){
queryFilter["dateDim"]=f;
refresh(queryFilter);
}
},
filterAll: function(){
}
};
var fakeNumByDate = {
all: function(){
try{
var dateFormat = d3.time.format("%Y-%m-%d");
filteredData["numByDate"].values.forEach(function (d) {
d['key'] = dateFormat.parse(d['key']);
});
}
catch(err) {
// console.log(err);
}
return filteredData["numByDate"].values;
},
order: function(){
},
top: function(){
}
};
// Make charts
timeChart
.width(1200)
.height(250)
.margins({top: 10, right: 50, bottom: 30, left: 50})
.dimension(fakeDateDim)
.group(fakeNumByDate)
.transitionDuration(500)
.x(d3.time.scale().domain([minDate, maxDate]))
.elasticY(true)
.renderHorizontalGridLines(true)
.yAxis().ticks(4);
timeChart
.filterHandler(function(dimension, filters){
if (filters) {
dimension.filter(filters);
}
else {
dimension.filter(null);
};
return filters;
});
// Similarly for typeChart
var typeChart = dc.rowChart("#dest-type-row-chart");
...
function init(){
d3.json("/refresh?filter={}", function(err, d){
filteredData = d;
dc.renderAll();
});
}
init();
I'm really stuck with why these two charts are not interacting with each other, any suggestions? (My apologies if they are obvious things)
Edit:
The data is loaded from a mongodb database.
The two plots are all shown correctly, so I don't think there is issue with the data itself and how they are grouped.
There are no errors being thrown. Server side debug shows (with each user click/drag):
GET /refresh?filter={%22dateDim%22:[[%222017-08-13T21:10:30.937Z%22,%222017-
09-08T16:02:52.755Z%22]],%22typeDim%22:
[%22Player%22,%22Schedule%22,%22Other%22,%22Game%20Stats%22]} 200 8.333 ms -
-
client side has no error shown.
The issue is, when you create two dc charts on selected dimensions and groups, they are supposed to interact/change according to the user action on either one of the two charts, but the way it's set up now, user action (clicks/drag) on either of the chart does not trigger the change to the other chart at all, the bar chart can still be clicked but nothing changes.
Related links I have also checked out:
BIG DATA VISUALIZATIONS USING CROSSFILTER AND DC.JS
Using dc.js on the clientside with crossfilter on the server
I'm attempting to not have to use a global variable called 'data'
In my js file I now have this function:
var Module_BarDataDaily = (function() {
var data;
d3.csv("myData.csv", function(rows) {
data = rows;
});
return {
dataX: data
}
})();
I was then (probably wrongly) under the impression that I can access this data via Module_BarDataDaily.dataX - so in a subsequent function I do the following:
function TOPbarChart(
grp, meas, colorChosen) {
TOPbarData = Module_BarDataDaily.dataX.map(function(d) { //line 900
return { ...
Console then just gives me an exception on line 900 of the following:
TypeError: Module_BarDataDaily.dataX is undefined
What am I doing wrong & how do I fix it?
The issue here is that d3.csv is asynchronous, so data is filled at a different time than accessed.
If you want to run d3.csv once, get the data and save them elsewhere, you can try something like this or this or this
In general:
// define your module here
var Module_BarDataDaily = function(data) {
this.dataX = data; //this.dataX will store the data
this.process = function(){// do whatever you need afterwards
console.log(this.dataX);
}
};
var process = function(myModule){
// or use another function to do what you need etc
console.log(myModule.dataX);
}
// load your csv once and save the data
d3.csv("path/to/your.csv", function(error, data) {
// after some proper error handling...
var myModule = new Module_BarDataDaily(data);
// so then you can do anything after that and
// your myModule will have data without calling d3.csv again
myModule.process();
//or
process(myModule);
});
Hope this helps!
Good luck!
I've used the following based on mkaran's answer:
var Module_BarDataDaily = function(data) {
this.dataX = data;
};
d3.csv("data/BarChart_data.csv", function(error, rows) {
var myModule = new Module_BarDataDaily(rows);
var chart = barChart();
chart.render(myModule.dataX);
d3.selectAll('input[name="meas"]').on("change", function change() {
chart.currentMeasure(this.value)
chart.render(myModule.dataX);
});
});
On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).
I've tried several different approaches, but I've been unsuccessful in having my view update its contents. My view contains a list of customers and a list of employees.
This represents what I've tried so far omitting employees as its essentially duplicated code.
function dbControl() {
var self=this;
self.customers = function() { $.ajax(...) }; // returns customers object
self.addCustomer = function() { $.ajax(...) };
self.delCustomer = function() { $.ajax(...) };
}
var DB = new dbControl();
var VM = {};
// Populate the VM
VM.selCustomer = ko.observable();
VM.customers = ko.mapping.fromJS(DB.customers);
VM.addCustomer = function() {
DB.addCustomer() // successfully adds to Database
VM.customers.push(); // push VM.selCustomer() into VM.customers
}
VM.delCustomer = function() {
DB.delCustomer() // succcessfully removes from Database
VM.customers.remove(); // removes VM.selCustomer() from VM.customers
}
// Knockout Binding
ko.applyBindings(VM);
The data-bind="foreach: customers" binding works on the webpage just fine and lists all the entries.
All the AJAX calls successfully LIST/ADD/DELETE from the Database properly, but the View does not update after a successful DELETE.
I've tried adding ko.mapping.fromJS(DB.customers, VM) to the end of the delete function in the VM as instructed in the DOCS:Then, every time you receive new data from the server, you can update all the properties on viewModel in one step by calling the ko.mapping.fromJS function again, but no luck.
I've tried adding VM.customers.valueHasMutated() to the VM and it still doesn't update.
I've tried creating an interval to run both previous attempts:
setInterval(function() {
VM.customers = [];
VM.customers = ko.mapping.fromJS(DB.customers);
VM.customers.valueHasMutated();
}, 1000);
Am I structuring this whole project wrong?
VM.addCustomer = function() {
DB.addCustomer() // successfully adds to Database
VM.customers.push(); // push VM.selCustomer() into VM.customers
}
VM.delCustomer = function() {
DB.delCustomer() // succcessfully removes from Database
VM.customers.remove(); // removes VM.selCustomer() from VM.customers
}
You're not passing anything to push or remove; those two methods of observableArray definitely want arguments. Depending on how the methods of DB are written, you may have the same problem with them.
Ok, so after more copy/pasting, I think I've solved my own problem:
The reason the View wasn't updating was because the data coming from the database was never being "re-called".
To fix the problem, within the add/delete functions in the VM, I had to call first the function that pulls the data from the server DB.getCustomers(), then make the ko.mapping.fromJS(DB.customers, {}, VM.customers) call.
function dbControl() {
var self = this;
getCustomers = function() {
$.ajax({
/* URL/TYPE/DATA/CACHE/ASYNC */
success: function() {
self.customers = data; // Now, DB.customers == Updated Data!
}
});
}
}
var DB = new dbControl();
VM = {};
VM.addCustomer = function() {
DB.addCustomer() // successfully adds to Database
//VM.customers.push(); <------ Remove the redundancy
DB.getCustomers(); // get the new list from the server
ko.mapping.fromJS(DB.customers, {}, VM.customers);
}
VM.delCustomer = function() {
DB.delCustomer() // succcessfully removes from Database
//VM.customers.remove(); <------ Remove the redundancy
DB.getCustomers(); // get the new list from the server
ko.mapping.fromJS(DB.customers, {}, VM.customers);
}