Asynchronous update of nodes properties after the graph is rendered (Vivagraph.js) - asynchronous

I am trying to populate nodes' data in a graph, asynchronously.
How to ensure that data fetched asyncrosly is actually bound to the graph, and rendered when ready?
First, you render the graph structure, node and links.
Second, you render data as nodes' properties, when data is ready.
New node can by dynamically added by interacting on parents' nodes, and don't want to wait for completion of node's properties.
Please note I am using Vivagraph.js library.graph is an object created with the library, addLinks() and getNode() are its function properties - see the demo at Vivagraph demo, I am using that as a draft of my attempts.
The issue I experience is that nodes are rendered in the graph as soon as they are added - addNode() either addLinks(node1, node2) functions -, while node's properties fetched asynchronously - getNode(node).property = updatedValue - result undefined.
EDITED - Simplified code based on comment
Below I include a working mockup version, based on tutorial provided by #Anvaka, the author of this (awesome) library.
My goal is to render the graph immediately, enabling interaction, and update data while it is being fetched from third parties.
// attempt 1: fetch data async
var fetchInfo = function (graph, nodeId) {
var root = 'http://jsonplaceholder.typicode.com';
$.ajax({
url: root + '/photos/' + nodeId,
method: 'GET'
}).then(function (data) {
graph.getNode(nodeId).data = data.thumbnailUrl;
console.log(graph.getNode(nodeId));
});
};
// attempt 2: defer ajax directly
var fetchInfo_2 = function (graph, nodeId) {
var root = 'http://jsonplaceholder.typicode.com';
return $.ajax({
url: root + '/photos/' + nodeId,
method: 'GET'
});
};
function main() {
// As in previous steps, we create a basic structure of a graph:
var graph = Viva.Graph.graph();
graph.addLink(1, 2);
fetchInfo(graph, 1); // updated data is undefined when graph is rendered
fetchInfo(graph, 2); // updated data is undefined when graph is rendered
/* trying a different outcome by deferring whole ajax
graph.getNode(1).data = fetchInfo_2(1).done(function(data) {
data.thumbnailUrl;
}); // the whole object is deferred but cannot fetch data
graph.getNode(2).data = fetchInfo_2(2).done(function(data) {
data.thumbnailUrl;
}); // the whole object is deferred but cannot fetch data
*/
var graphics = Viva.Graph.View.svgGraphics(),
nodeSize = 24,
addRelatedNodes = function (nodeId, isOn) {
for (var i = 0; i < 6; ++i) {
var child = Math.floor((Math.random() * 150) + nodeId);
// I add children and update data from external sources
graph.addLink(nodeId, child);
fetchInfo(graph, child);
}
};
// dynamically add nodes on mouse interaction
graphics.node(function (node) {
var ui = Viva.Graph.svg('image')
.attr('width', nodeSize)
.attr('height', nodeSize)
.link(node.data);
console.log('rendered', node.id, node.data);
$(ui).hover(function () {
// nodes are rendered; nodes' data is undefined
addRelatedNodes(node.id);
});
return ui;
}).placeNode(function (nodeUI, pos) {
nodeUI.attr('x', pos.x - nodeSize / 2).attr('y', pos.y - nodeSize / 2);
});
graphics.link(function (link) {
return Viva.Graph.svg('path')
.attr('stroke', 'gray');
}).placeLink(function (linkUI, fromPos, toPos) {
var data = 'M' + fromPos.x + ',' + fromPos.y +
'L' + toPos.x + ',' + toPos.y;
linkUI.attr("d", data);
})
var renderer = Viva.Graph.View.renderer(graph, {
graphics: graphics
});
renderer.run();
}
main();
svg {
width: 100%;
height: 100%;
}
<script src="https://rawgit.com/anvaka/VivaGraphJS/master/dist/vivagraph.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>

As I said in the comments, any work you want to do as a result of an asynchronous process must be done in the callback. This includes any rendering work.
For jQuery's deferreds (like the result of an Ajax call), the callback is defined through .then or .done, enabling you to detach the act of fetching a resource from working with that resource.
Setting the image source is rendering work, so you must do it in the callback. Below is a function that fetches images and returns the deferred result and the node callback function uses that to do its own piece of work.
function fetchInfo(id) {
var root = 'http://jsonplaceholder.typicode.com';
return $.getJSON(root + '/photos/' + id);
}
function main() {
var graph = Viva.Graph.graph(),
graphics = Viva.Graph.View.svgGraphics(),
nodeSize = 24,
addRelatedNodes = function (nodeId, count) {
var childId, i;
for (i = 0; i < count; ++i) {
childId = Math.floor(Math.random() * 150) + nodeId;
graph.addLink(nodeId, childId);
}
};
graphics
.node(function (node) {
var ui = Viva.Graph.svg('image')
.attr('width', nodeSize)
.attr('height', nodeSize)
.link('http://www.ajaxload.info/images/exemples/24.gif');
$(ui).dblclick(function () {
addRelatedNodes(node.id, 6);
});
// render when ready
fetchInfo(node.id).done(function (data) {
ui.link(data.thumbnailUrl);
});
return ui;
})
.placeNode(function (nodeUI, pos) {
nodeUI.attr('x', pos.x - nodeSize / 2).attr('y', pos.y - nodeSize / 2);
})
.link(function (link) {
return Viva.Graph.svg('path')
.attr('stroke', 'gray');
})
.placeLink(function (linkUI, fromPos, toPos) {
var data =
'M' + fromPos.x + ',' + fromPos.y +
'L' + toPos.x + ',' + toPos.y;
linkUI.attr("d", data);
});
graph.addLink(1, 2);
Viva.Graph.View.renderer(graph, {
graphics: graphics
}).run();
}
main();
svg {
width: 100%;
height: 100%;
}
img {
cursor: pointer;
}
<script src="https://rawgit.com/anvaka/VivaGraphJS/master/dist/vivagraph.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
Sadly, it does not seem to run in the StackSnippets, but it works over on jsFiddle: http://jsfiddle.net/tL9992ua/

Related

Aframe: cycle through colors using array

I've been trying to cycle through colors using a custom component.
<script>
AFRAME.registerComponent('floor-cycle', {
init: function () {
var sceneEl = document.querySelector('a-scene');
var floorEl = sceneEl.querySelector('#floor')
status = 1;
floorEl.addEventListener('click', function () {
if(status==1) {
floorEl.setAttribute('color', 'red'); status = 2
}
else if(status==2) {
floorEl.setAttribute('color', 'blue'); status = 3;
}
else if(status==3) {
floorEl.setAttribute('color', 'green'); status = 1
}
}
);
}
});
</script>
The component uses status to set the color attribute on click event, however this seems inefficient. Can this be implemented using an array rather than status?
demo - https://codepen.io/MannyMeadows/pen/GWzJRB
You can make an array ['red','green','blue'] and Cycle through it:
colors = ['red','green','blue'];
let i = 0;
floorEl.addEventListener('click',function(){
floorEl.setAttribute('material','color', colors[i]);
function add(){(i==colors.length-1) ? i = 0 : i++;}
add();
});
Seems better as the array is now dynamic, not sure how about the performance.
working fiddle here: https://jsfiddle.net/gftruj/g9wfLgab/2/

How to make TagsInput to work with both auto complete & free text

UPDATE
This issue is already discussed in github here
I am using tagsinput with typeahead in bootstrap 3. The problem which I am experiencing is with the value in case if user selects the existing tag. Display text shows it right but .val() returns its actual object. Below is the code
$('#tags').tagsinput({
//itemValue: 'value',
typeahead: {
source: function (query) {
//tags = [];
//map = {};
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query)
//, function (data) {
// $.each(data, function (i, tag) {
// map[tag.TagValue] = tag;
// tags.push(tag.TagValue);
// });
// return process(tags);
//});
}
}
//freeElementSelector: "#freeTexts"
});
The problem with above code is that it results as below while fetching tags from web method
This happens when user select the existing tag. New tags no issues. I tried setting itemValue & itemText of tagsinput but not worked. Hence I decided a work-around of this problem. Since I could able get the json string as ['IRDAI", Object], if can somehow parse these object & get the actual tag value then I get the expected result of the code I am looking at.
Below is what it appears in tags input as [object Object] for text selected by user from auto populated drop down
[![enter imt
If I i specify TagId & TagValue to itemValue & itemText as below code
$('#tags').tagsinput({
itemValue: 'TagId',
itemText: 'TagValue',
typeahead: {
source: function (query) {
//tags = [];
//map = {};
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query)
//, function (data) {
// $.each(data, function (i, tag) {
// //map[tag.TagValue] = tag;
// tags.push(tag.TagValue);
// });
//});
// return process(tags);
}
}
//freeElementSelector: "#freeTexts"
});
Then the result is displaying as below when below code is executed
var arr = junit.Tags.split(',');
for (var i = 0; i < arr.length; i++) {
$('#tags').tagsinput('add', arr[i]);
}
Given your example JSON response from your data source:
[
{"TagId":"1", "TagValue":"eSign"},
{"TagId":"2", "TagValue":"eInsurance Account"}
]
You'll need to tell tagsinput how to map the attributes from your response objects using itemValue and itemText in your tagsinput config object. It looks like you may have started down that path, but didn't reach the conclusion, which should look something like:
$('#tags').tagsinput({
itemValue: 'TagId',
itemText: 'TagValue',
typeahead: {
source: function (query) {
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query);
}
}
});
Be sure to checkout the tagsinput examples.
This may not be the clean solution but I got around this issue through below parsing method. Hope this helps someone.
var items = $('#tags').tagsinput("items");
var tags = '';
for(i = 0; i < items.length; i++)
{
if(JSON.stringify(items[i]).indexOf('{') >= 0) {
tags += items[i].TagValue;
tags += ',';
} else {
tags += items[i];
tags += ',';
}
}

Meteor template autorun (Session variable)

Imagine I have a session variable that holds an image source. Every second, I want to the helper that contains this session to run.
if (Meteor.isClient) {
Template.TargetTask.helpers({
'imageSrc': function (e, template) {
var clock = setTimeout(function() {
var position = IMAGE_POOL.pop();
Session.set("currentTarget", position);
}, 1000);
var position = Session.get("currentTarget");
var imageSrc = '/' + position + '.bmp';
return imageSrc;
}
});
the image sources are coming from a global IMAGE_POOL. However, it is possible that the pool contains two same images consecutively. In this case, Session.set() will be called with the same argument and the session will remain unchanged.
Q1. When Session variable remains unchanged, does the template helper not autorun even if Session.set() is called?
Q2. If so, how should I make it run every time a new image is popped?
No, Tracker computations are not invalidated if the value doesn't change.
Session.set('test', false);
Tracker.autorun(function() { console.log(Session.get('test')) }); //Logs 'false'
Session.set('test', false); //Nothing
Session.set('test', true); //Logs true
In your case, if you want to preserve this code structure (which seems a bit heavy to me) you could instead store an object with a timestamp:
if (Meteor.isClient) {
Template.TargetTask.helpers({
'imageSrc': function (e, template) {
var clock = setTimeout(function() {
var position = IMAGE_POOL.pop();
Session.set("currentTarget", {
position : position,
timestamp : Date.now()
});
}, 1000);
var position = Session.get("currentTarget").position;
var imageSrc = '/' + position + '.bmp';
return imageSrc;
}
});
}

Filter results from Google Autocomplete

Is there a way to get the results from Google Autocomplete API before it's displayed below the input? I want to show results from any country except U.S.A.
I found this question: Google Maps API V3 - Anyway to retrieve Autocomplete results instead of dropdown rendering it? but it's not useful, because the method getQueryPredictions only returns 5 elements.
This is an example with UK and US Results: http://jsfiddle.net/LVdBK/
Is it possible?
I used the jquery autocomplete widget and called the google methods manually.
For our case, we only wanted to show addresses in Michigan, US.
Since Google doesn't allow filtering out responses to that degree you have to do it manually.
Override the source function of the jquery autocomplete
Call the google autocompleteService.getQueryPredictions method
Filter out the results you want and return them as the "response" callback of the jquery autocomplete.
Optionally, if you need more detail about the selected item from Google, override the select function of the jquery autocomplete and make a call to Google's PlacesService.getDetails method.
The below assumes you have the Google api reference with the "places" library.
<script src="https://maps.googleapis.com/maps/api/js?key=[yourKeyHere]&libraries=places&v=weekly" defer></script>
var _autoCompleteService; // defined globally in script
var _placesService; // defined globally in script
//...
// setup autocomplete wrapper for google places
// starting point in our city
var defaultBounds = new google.maps.LatLngBounds(
new google.maps.LatLng('42.9655426','-85.6769166'),
new google.maps.LatLng('42.9655426','-85.6769166'));
if (_autoCompleteService == null) {
_autoCompleteService = new google.maps.places.AutocompleteService();
}
$("#CustomerAddress_Street").autocomplete({
minLength: 2,
source: function (request, response) {
if (request.term != '') {
var googleRequest = {
input: request.term,
bounds: defaultBounds,
types: ["geocode"],
componentRestrictions: { 'country': ['us'] },
fields: ['geometry', 'formatted_address']
}
_autoCompleteService.getQueryPredictions(googleRequest, function (predictions) {
var michiganOnly = new Array(); // array to hold only addresses in Michigan
for (var i = 0; i < predictions.length; i++) {
if (predictions[i].terms.length > 0) {
// find the State term. Could probably assume it's predictions[4], but not sure if it is guaranteed.
for (var j = 0; j < predictions[i].terms.length; j++) {
if (predictions[i].terms[j].value.length == 2) {
if (predictions[i].terms[j].value.toUpperCase() == 'MI') {
michiganOnly.push(predictions[i]);
}
}
}
}
}
response(michiganOnly);
});
}
},
select: function (event, ui) {
if (ui != null) {
var item = ui.item;
var request = {
placeId: ui.item.place_id
}
if (_placesService == null) {
$("body").append("<div id='GoogleAttribution'></div>"); // PlacesService() requires a field to put it's attribution image in. For now, just put on on the body
_placesService = new google.maps.places.PlacesService(document.getElementById('GoogleAttribution'));
}
_placesService.getDetails(request, function (result, status) {
if (result != null) {
const place = result;
if (!place.geometry) {
// User entered the name of a Place that was not suggested and
// pressed the Enter key, or the Place Details request failed.
//window.alert("No details available for input: '" + place.name + "'");
return;
}
else {
var latitude = place.geometry.location.lat();
var longitude = place.geometry.location.lng();
// do something with Lat/Lng
}
}
});
}
}
}).autocomplete("instance")._renderItem = function (ul, item) {
// item is the prediction object returned from our call to getQueryPredictions
// return the prediction object's "description" property or do something else
return $("<li>")
.append("<div>" + item.description + "</div>")
.appendTo(ul);
};
$("#CustomerAddress_Street").autocomplete("instance")._renderMenu = function (ul, items) {
// Google's terms require attribution, so when building the menu, append an item pointing to their image
var that = this;
$.each(items, function (index, item) {
that._renderItemData(ul, item);
});
$(ul).append("<li class='ui-menu-item'><div style='display:flex;justify-content:flex-end;'><img src='https://maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white3.png' /></div></li>")
}

Changing Google Maps v3 MarkerClustererPlus Title

I am trying to dynamically set the cluster title, rollover text, of clustered icons. I want the cluster count/total to be used in the rollover text.
Through console.log I am able to see that the title has been changed to that set for var txt. It also works with alert( txt ). The default title for a cluster is "" and does not seem to be getting updated and stays at the default value.
Currently I am setting the title in google.maps.event.addListener( markerClusterer, 'mouseover', function( cluster ) {}).
I'm thinking that my code continues to execute and that might be the reason I don't see the change but I haven't been able to narrow it down.
var latlng = new google.maps.LatLng( lat, lng );
var qs = location.search;
var options = {
zoom: 17,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP,
mapTypeControlOptions: {
style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
}
};
map = new google.maps.Map( mapId[0], options );
google.maps.event.addListener( map, 'idle', function() {
var bounds = map.getBounds();
downloadXML( ABSPATH + 'xml/maps/markers.php' + qs + '&bounds=' + bounds, function( data ) {
var xml = parseXml( data );
var markers = xml.documentElement.getElementsByTagName( "marker" );
var markerArray = [];
for ( var i = 0; i < markers.length; i++ ) {
var attributes = getMarkerAttributes( markers[i] );
var marker = createMarker( attributes );
// Add marker to marker array
markerArray.push(marker);
}
// Define the marker clusterer
var clusterOptions = {
zoomOnClick: false,
gridSize: 1
}
var markerClusterer = new MarkerClusterer( map, markerArray, clusterOptions );
// Listen for a cluster to be clicked
google.maps.event.addListener( markerClusterer, 'clusterclick', function( cluster ) {
combineInfoWindows( cluster );
});
// Listen for a cluster to be hovered and set title
google.maps.event.addListener( markerClusterer, 'mouseover', function( cluster ) {
var txt = 'There are ' + cluster.getSize() + ' properties at this location.';
//alert( txt );
console.log( cluster );
markerClusterer.setTitle( txt );
});
}); // downloadXML
}); // google.maps.event.addListener( map, 'idle', ... )
Any help would be greatly appreciated. Thanks!
EDIT: 1
I have a solution based on the suggested solution by Rick.
I have modified the onAdd method.
/**
* Adds the icon to the DOM.
*/
ClusterIcon.prototype.onAdd = function () {
var cClusterIcon = this;
// MY CHANGES - START
this.cluster_.markerClusterer_.title_ = 'There are ' + this.cluster_.getSize() + ' properties at this location.';
// MY CHANGES - END
this.div_ = document.createElement("div");
if (this.visible_) {
this.show();
}
...
};
EDIT: 2 - FINAL SOLUTION
Moved changes to show method versus previous onAdd method as Rick had suggested. Change is made in a file outside of the original source file for MarkerClustererPlus.
/**
* Positions and shows the icon.
*/
ClusterIcon.prototype.show = function () {
if (this.div_) {
var pos = this.getPosFromLatLng_(this.center_);
this.div_.style.cssText = this.createCss(pos);
if (this.cluster_.printable_) {
// (Would like to use "width: inherit;" below, but doesn't work with MSIE)
this.div_.innerHTML = "<img src='" + this.url_ + "'><div style='position: absolute; top: 0px; left: 0px; width: " + this.width_ + "px;'>" + this.sums_.text + "</div>";
} else {
this.div_.innerHTML = this.sums_.text;
}
//this.div_.title = this.cluster_.getMarkerClusterer().getTitle();
// MY SOLUTION BELOW
this.div_.title = 'There are ' + this.cluster_.getSize() + ' properties at this location.';
this.div_.style.display = "";
}
this.visible_ = true;
};
Are you using this for clustering markers? Did you extend it to make your own setTitle function? If not, You'll have to make your own label. It's not actually a marker per se.
Edit: Didn't know this existed.
The cluster icons just pull the title from the MCOptions. I don't see where ClusterIcon or Cluster has a setTitle function, so I'd think the best bet would be overriding the ClusterIcon show prototype yourself and setting it there.
> ClusterIcon.prototype.show =
> function () { if (this.div_) {
> var pos = this.getPosFromLatLng_(this.center_);
> this.div_.style.cssText = this.createCss(pos);
> if (this.cluster_.printable_) {
> // (Would like to use "width: inherit;" below, but doesn't work with MSIE)
> this.div_.innerHTML = "<img src='" + this.url_ + "'><div style='position: absolute; top: 0px; left: 0px; width: " + this.width_
> + "px;'>" + this.sums_.text + "</div>";
> } else {
> this.div_.innerHTML = this.sums_.text;
> }
> this.div_.title = **** Your stuff here ***
> this.div_.style.display = ""; } this.visible_ = true; };
Your problem is that you are trying to assign the mouseover event listener (where you set the title) to a MarkerClusterer, but to define a mouseover listener, you have to pass a Cluster.
There is a MarkerClusterer.getClusters() function that will return an Array of the Cluster instances. You want to loop over that Array and pass an instance of Cluster to your mouseover event listeners. If you check the reference doc and scroll down to the MarkerClusterer Events section of the doc, the row for mouseover defines the Argument to be:
c:Cluster
Which is in contrast to events like clusteringbegin and clusteringend, which define the Argument to be:
mc:MarkerClusterer
Having said all that, I'm not sure there is an easy way to set the title for each Cluster. That class does not have a setTitle function. The setTitle on the MarkerClusterer just applies the title to all of the Cluster instances. And I've double checked in JavaScript; there is no setTitle function on the Cluster class. Your best option right now seems to be to dynamically create the content you want to display within the mouseover handler for each Cluster. You could create an InfoBox and then close it on a Cluster mouseoet event. Not the simplest solution ever, but it will get you where you want to be.
I know this is an old question, but it ranked high on Google for my search. Anyway, here is what I did thanks to tips from this page:
google.maps.event.addListener(markerCluster, 'mouseover', function (c) {
if(c.clusterIcon_.div_){
c.clusterIcon_.div_.title = c.getSize() + ' businesses in this area';
}
});
I can't guarantee it will be compatible with future versions of MarkerClusterer Plus since I'm using "private" properties clusterIcon_ and div_.

Resources