arbor js - save and load graph - javascript

I'm using arbor js to create diagrams using its force-based alorithm.
I would like to be able to save the x,y system coordinates of the nodes after the total energy has got below a certain point, so that I can load them back in when accessing the graph, to avoid having to re-process all the energy from scratch.
I have had two problems doing this:
sys.energy().sum always returns NaN
as an alternative I decided to use setTimeout to save the graph node positions after a given period of time has elapsed
While I have been able to save the nodes x,y system coordinates to DB, when I try to load the data into the graph (using sys.merge or sys.addNode) with saved x y coordinates, the graph fails to display, and an 'out of memory' message appears in the console log.
Here is an example of data with node x,y system coordinates that were saved, and which causes arbor js to crash which loading:
{"nodes":{"0":{"edgeCount":0,"x":13.11901,"y":14.89151,"id":0,"name":"Global"},"23":
{"edgeCount":1,"x":18.08981,"y":-0.8355745,"id":23,"name":"Aristotle"},"26":
{"edgeCount":1,"x":5.688836,"y":14.86863,"id":26,"name":"Socrates"},"27":
{"edgeCount":1,"x":14.84461,"y":-1.687457,"id":27,"name":"Christianity"},"34":
{"edgeCount":1,"x":-2.265221,"y":13.59168,"id":34,"name":"Maths"},"91":
{"edgeCount":11,"x":5.868572,"y":-1.315289,"id":91,"name":"Plato"},"92":
{"edgeCount":4,"x":-3.14131,"y":8.81194,"id":92,"name":"Virtue"},"127":
{"edgeCount":1,"x":-3.308347,"y":8.463552,"id":127,"name":"Poetry"},"131":
{"edgeCount":1,"x":7.605158,"y":-3.951363,"id":131,"name":"Wisdom"},"147":
{"edgeCount":1,"x":6.195698,"y":-4.572639,"id":147,"name":"Person"},"149":
{"edgeCount":1,"x":2.10395,"y":-2.390886,"id":149,"name":"Republic"},"171":
{"edgeCount":1,"x":3.359434,"y":-3.996424,"id":171,"name":"Justice"},"172":
{"edgeCount":1,"x":-0.2544371,"y":-4.218832,"id":172,"name":"Temperance"},"173":
{"edgeCount":1,"x":-8.773163,"y":2.587845,"id":173,"name":"Courage"},"178":
{"edgeCount":1,"x":-2.380451,"y":-5.787674,"id":178,"name":"Schopenhauer"},"194":
{"edgeCount":2,"x":-7.747643,"y":-1.653015,"id":194,"name":"Beauty"},"195":
{"edgeCount":1,"x":-10.45985,"y":-2.670299,"id":195,"name":"Objective"},"196":
{"edgeCount":1,"x":-10.64573,"y":-6.874766,"id":196,"name":"Truth"},"219":
{"edgeCount":1,"x":-6.22574,"y":-7.565969,"id":219,"name":"Theory of forms"},"221":
{"edgeCount":1,"x":-8.314561,"y":-7.570002,"id":221,"name":"Platinus"},"245":
{"edgeCount":0,"x":-13.20351,"y":-8.421284,"id":245,"name":"Diagram"},"254":
{"edgeCount":1,"x":-13.54734,"y":-7.7437,"id":254,"name":"Green"}},
"edges":{"23":{"91":{"context":{"id":0,"name":"Global"},"source":
{"id":23,"name":"Aristotle"},"predicate":{"id":21,"name":"studied with"},"target":
{"id":91,"name":"Plato"}}},"26":{"91":{"context":{"id":0,"name":"Global"},"source":
{"id":26,"name":"Socrates"},"predicate":{"id":2,"name":"inspires"},"target":
{"id":91,"name":"Plato"}}},"91":{"149":{"context":{"id":0,"name":"Global"},"source":
{"id":91,"name":"Plato"},"predicate":{"id":1,"name":"writes"},"target":
{"id":149,"name":"Republic"}},"219":{"context":{"id":0,"name":"Global"},"source":
{"id":91,"name":"Plato"},"predicate":{"id":1,"name":"writes"},"target":{"id":219,"name":"Theory of forms"}},"27":{"context":{"id":0,"name":"Global"},"source":{"id":91,"name":"Plato"},"predicate":
{"id":3,"name":"influences"},"target":{"id":27,"name":"Christianity"}},"178":{"context":
{"id":0,"name":"Global"},"source":{"id":91,"name":"Plato"},"predicate":
{"id":3,"name":"influences"},"target":{"id":178,"name":"Schopenhauer"}},"221":{"context":
{"id":0,"name":"Global"},"source":{"id":91,"name":"Plato"},"predicate":
{"id":3,"name":"influences"},"target":{"id":221,"name":"Platinus"}},"254":{"context":
{"id":245,"name":"Diagram"},"source":{"id":91,"name":"Plato"},"predicate":
{"id":28,"name":"is"},"target":{"id":254,"name":"Green"}},"34":{"context":
{"id":0,"name":"Global"},"source":{"id":91,"name":"Plato"},"predicate":{"id":33,"name":"is associated with"},"target":{"id":34,"name":"Maths"}},"127":{"context":
{"id":0,"name":"Global"},"source":{"id":91,"name":"Plato"},"predicate":{"id":47,"name":"is obsessed with"},"target":{"id":127,"name":"Poetry"}},"147":{"context":
{"id":0,"name":"Global"},"source":{"id":91,"name":"Plato"},"predicate":{"id":56,"name":"is type of"},"target":{"id":147,"name":"Person"}}},"92":{"131":{"context":{"id":91,"name":"Plato"},"source":
{"id":92,"name":"Virtue"},"predicate":{"id":28,"name":"is"},"target":
{"id":131,"name":"Wisdom"}},"171":{"context":{"id":91,"name":"Plato"},"source":
{"id":92,"name":"Virtue"},"predicate":{"id":28,"name":"is"},"target":
{"id":171,"name":"Justice"}},"172":{"context":{"id":91,"name":"Plato"},"source":
{"id":92,"name":"Virtue"},"predicate":{"id":28,"name":"is"},"target":
{"id":172,"name":"Temperance"}},"173":{"context":{"id":91,"name":"Plato"},"source":
{"id":92,"name":"Virtue"},"predicate":{"id":28,"name":"is"},"target":
{"id":173,"name":"Courage"}}},"194":{"195":{"context":{"id":91,"name":"Plato"},"source":
{"id":194,"name":"Beauty"},"predicate":{"id":28,"name":"is"},"target":
{"id":195,"name":"Objective"}},"196":{"context":{"id":91,"name":"Plato"},"source":
{"id":194,"name":"Beauty"},"predicate":{"id":33,"name":"is associated with"},"target":
{"id":196,"name":"Truth"}}}}}
I can find no examples of graphs where the node x,y coordinates are in the input data.

Looking at https://github.com/samizdatco/arbor/blob/master/src/physics/system.js#L83 it seems that it should be alright to include x and y value in the data dictionary. I'm not really sure from your example how you pass the data to the methods. Maybe try just adding a few of the nodes and see where they are placed. I have the same problem with the energy being NaN. Probably using a debugger will help.

Related

Chart.js 2.2.x - How to access the pixel coordinate of a point in my dataset

I'm creating a plugin for my chart (in ChartJS 2.2.2), and for that I need the pixel position of each point of my dataset.
When inspecting the chart.config.data.datasets[0]._meta[0], I get back the following object:
But when I access the x field directly in my plugin code, the values returned are different. The commands:
chart.config.data.datasets[0]._meta[0].dataset._children[0]._model.x and
chart.config.data.datasets[0]._meta[0].data[0]._model.x (which yields the same value when looking it up in the chrome debugger) both return x = 3 instead of 44.67.
Why is this happening? What is the proper way to access the pixel coordinate of a point in my dataset?
The plugin is called multiple times, I was debugging only the initial values for the coordinates. Turns out the last calls were made with the correct value for X (44.67). I think this was happening because initially the canvas containing the chart was hidden (using ng-hide), only after init it was being shown.
It was rendered correctly though.

How do I create a leaflet map with thousands of marks that doesn't crash my browser?

I'm using the leaflet package in R to generate a map with a large number of circles on it. The goal is a map I can publish to my website. The problem I'm having is that as I increase the number of circles, the resulting map loads very slowly, I get "unresponsive script" warnings and ultimately it completely freezes up my browser.
I know this sort of thing is possible, because I've found a leaflet map that works the way I want mine to work:
http://cartologic.com/geoapps/map_viewer/5/ny-crimes-2014-dot-density-map
I notice on the above map that the circles don't appear "clickable" like the circles on my map, and that they seem to load in square chunks. I have a hunch that these things are related to my problem. Unfortunately, I'm too much of a novice at leaflet/javascript stuff to figure this out on my own.
Here is a toy example illustrating my problem:
library("leaflet")
library("htmlwidgets")
dots <- data.frame(x=c(runif(10000, -93.701281, -93.533053)),
y=c(runif(10000, 41.515962, 41.644369)))
m <- leaflet(dots) %>%
addTiles('http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png') %>%
setView(-93.617167, 41.580166, zoom = 12) %>%
addCircles(~x, ~y, weight = 1, radius = 5,
color = "#FFA500", stroke = TRUE, fillOpacity = 0.1)
m
saveWidget(widget = m, file="example.html", selfcontained = TRUE)
mapview can help you here. It builds upon the leaflet library for smaller data sets, but uses special javascript functionality for larger data.
your example with 1 Mio. points:
library(mapview)
library(sp)
dots <- data.frame(x=c(runif(1000000, -93.701281, -93.533053)),
y=c(runif(1000000, 41.515962, 41.644369)))
coordinates(dots) <- ~ x + y
proj4string(dots) <- "+init=epsg:4326"
mapview(dots)
It may still take a while to render, but once rendered it should be quite responsive. Note that mapview is designed to work with spatial* objects, that is why we need the calls to set the coordinate slot and the projection.
For more information have a look here:
http://environmentalinformatics-marburg.github.io/web-presentations/20150723_mapView.html
Hope that helps.
If you want to add a large number of vector objects to a map, it is rare that it can be done easily.
Notice that the raster data is broken into tiles so that all the information does not have to be shown at one time. For your vector data (in this case, circles) you have to do the same thing.
Basically what I like to do is to break the large data set into smaller (vector) tiles, with the same boundaries as the raster tiles you are showing. Duplicate the data if you want it to appear at several zoom level. As you are showing circle, imagine that you partition the circles' center points on the tile boundary.
I have an application similar to this where I basically partition my vector data on tile boundaries and store the information in geojson files. When I get an event that the raster tile has been loaded I can then load the equivalent vector file as a geojson layer (same thing when the raster tile is unloaded). In this way, you can limit the amount of vector data that has to be displayed at any one time.
If you have a lot of points, they are not really going to be visible at low zoom levels anyway, so it might be better just to show them at an appropriate zoom level (perhaps with a different representation at low zooms - like a heat map). This will keep the amount of data being shown at any one time lower.
Since this question has a few upvotes, I'll generally describe both of the solutions I found. Maybe if I have time later I'll get all the files together on GitHub.
First, I found TileMill. Simply load a data file of coordinates into TileMill, style the way you want them to appear, and output tiles (png). Host those tiles on the web somewhere and load them with leaflet. This process was a bit too manual for my liking because TileMill kept crashing when I loaded in csv files that were too large for it to render on my machine.
I found the best solution was use Processing, adapting Robert Manduca's code here: https://github.com/rmanduca/jobmaps. I don't use Python so I rewrote those parts in R and modified the Processing code according to my specifications.
Mapdeck (released on CRAN Aug 2018) uses WebGL (through Deck.gl) and is designed to handle millions of points (depending on your system's hardware of course)
library(mapdeck)
set_token("MAPBOX_TOKEN")
n <- 1e6
dots <- data.frame(x=c(runif(n, -93.701281, -93.533053)),
y=c(runif(n, 41.515962, 41.644369)))
dots$letter <- sample(letters, size = n, replace = T)
mapdeck(
style = mapdeck_style('dark')
) %>%
add_scatterplot(
data = dots
, lon = "x"
, lat = "y"
, fill_colour = "letter"
, radius = 5
, fill_opacity = 50
, layer_id = "dots"
)

d3.v3 scatterplot with all circles the same radius

Every example I have found shows all of the scatter plot points to be of random radii. Is it possible to have them all the same size? If I try to statically set the radius all of the circles will be very small (I'm assuming the default radius). However, if I use Math.random() as in most examples there are circles large and small. I want all the circles to be large. Is there a way to do that? Here's the code snippet forming the graph data using Math.random() (this works fine for some reason):
function scatterData(xData, yData)
{
var data = [];
for (var i = 0; i < seismoNames.length; i++)
{
data.push({
key: seismoNames[i],
values: []
});
var xVals=""+xData[i];
xVals=xVals.split(",");
var yVals=""+yData[i];
yVals=yVals.split(",");
for (var j = 0; j < xVals.length; j++)
{
data[i].values.push({
x: xVals[j],
y: yVals[j],
size: Math.random()
});
}
}
return data;
}
Math.random() spits out values between 0 and 1 such as 0.164259538891095 and 0.9842195005008699. I have tried putting these as static values in the 'size' attribute, but no matter what the circles are always really small. Is there something I'm missing?
Update: The NVD3 API has changed, and now uses pointSize, pointSizeDomain, etc. instead of just size. The rest of the logic for exploring the current API without complete documentation still applies.
For NVD3 charts, the idea is that all adjustments you make can be done by calling methods on the chart function itself (or its public components) before calling that function to draw the chart in a specific container element.
For example, in the example you linked too, the chart function was initialized like this:
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.color(d3.scale.category10().range());
chart.xAxis.tickFormat(d3.format('.02f'));
chart.yAxis.tickFormat(d3.format('.02f'));
The .showDistX() and .showDistY() turn on the tick-mark distribution in the axes; .color() sets the series of colours you want to use for the different categories. The next too lines access the default axis objects within the chart and set the number format to be a two-digit decimal. You can play around with these options by clicking on the scatterplot option from the "Live Code" page.
Unfortunately, the makers of the NVD3 charts don't have a complete documentation available yet describing all the other options you can set for each chart. However, you can use the javascript itself to let you find out what methods are available.
Inspecting a NVD3.js chart object to determine options
Open up a web page that loads the d3 and nvd3 library. The live code page on their website works fine. Then open up your developer's console command line (this will depend on your browser, search your help pages if you don't know how yet). Now, create a new nvd3 scatter chart function in memory:
var testChart = nv.models.scatterChart();
On my (Chrome) console, the console will then print out the entire contents of the function you just created. It is interesting, but very long and difficult to interpret at a glance. And most of the code is encapsulated so you can't change it easily. You want to know which properties you can change. So run this code in the next line of your console:
for (keyname in testChart){console.log(keyname + " (" + typeof(testChart[keyname]) + ")");}
The console should now print out neatly the names of all the methods and objects that you can access from that chart function. Some of these will have their own methods and objects you can access; discover what they are by running the same routine, but replacing the testChart with testChart.propertyName, like this:
for (keyname in testChart.xAxis){console.log(keyname + " (" + typeof(testChart.xAxis[keyname]) + ")");}
Back to your problem. The little routine I suggested above doesn't sort the property names in any order, but skimming through the list you should see three options that relate to size (which was the data variable that the examples were using to set radius)
size (function)
sizeDomain (function)
sizeRange (function)
Domain and range are terms used by D3 scales, so that gives me a hint about what they do. Since you don't want to scale the dots, let's start by looking at just the size property. If you type the following in the console:
testChart.size
It should print back the code for that function. It's not terribly informative for what we're interested in, but it does show me that NVD3 follows D3's getter/setter format: if you call .property(value) you set the property to that value, but if you call .property() without any parameters, it will return back the current value of that property.
So to find out what the size property is by default, call the size() method with no parameters:
testChart.size()
It should print out function (d) { return d.size || 1}, which tells us that the default value is a function that looks for a size property in the data, and if it doesn't exist returns the constant 1. More generally, it tells us that the value set by the size method determines how the chart gets the size value from the data. The default should give a constant size if your data has no d.size property, but for good measure you should call chart.size(1); in your initialization code to tell the chart function not to bother trying to determine size from the data and just use a constant value.
Going back to the live code scatterplot can test that out. Edit the code to add in the size call, like this:
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.color(d3.scale.category10().range())
.size(1);
chart.xAxis.tickFormat(d3.format('.02f'));
chart.yAxis.tickFormat(d3.format('.02f'));
Adding that extra call successfully sets all the dots to the same size -- but that size is definitely not 1 pixel, so clearly there is some scaling going on.
First guess for getting bigger dots would be to change chart.size(1) to chart.size(100). Nothing changes, however. The default scale is clearly calculating it's domain based on the data and then outputting to a standard range of sizes. This is why you couldn't get big circles by setting the size value of every data element to 0.99, even if that would create a big circle when some of the data was 0.01 and some was 0.99. Clearly, if you want to change the output size, you're going to have to set the .sizeRange() property on the chart, too.
Calling testChart.sizeRange() in the console to find out the default isn't very informative: the default value is null (nonexistent). So I just made a guess that, same as the D3 linear scale .range() function, the expected input is a two-element array consisting of the max and min values. Since we want a constant, the max and min will be the same. So in the live code I change:
.size(1);
to
.size(1).sizeRange([50,50]);
Now something's happening! But the dots are still pretty small: definitely not 50 pixels in radius, it looks closer to 50 square pixels in area. Having size computed based on the area makes sense when sizing from the data, but that means that to set a constant size you'll need to figure out the approximate area you want: values up to 200 look alright on the example, but the value you choose will depend on the size of your graph and how close your data points are to each other.
--ABR
P.S. I added the NVD3.js tag to your question; be sure to use it as your main tag in the future when asking questions about the NVD3 chart functions.
The radius is measured in pixels. If you set it to a value less than one, yes, you will have a very small circle. Most of the examples that use random numbers also use a scaling factor.
If you want all the circles to have a constant radius you don't need to set the value in the data, just set it when you add the radius attribute.
Not sure which tutorials you were looking at, but start here: https://github.com/mbostock/d3/wiki/Tutorials
The example "Three little circles" does a good step-by-step of the different things you can do with circles:
http://mbostock.github.io/d3/tutorial/circle.html

Create SVGPoint inside an element with user coordinate

I have a small project (to learn SVG) running (using javascript).
I would like to be able to track a point in a shape with its own user coordinate system. My idea is to find the coordinates of the point within the shape, then create an SVGPoint, so that I can pass on that element. I have seen the method create SVGPoint in examples, but it seems it is used in the context of the 'SVG_root' (that is, document.documentElement.createSVGPoint() works).
When I use (in Firefox)
inSvgObj.createSVGPoint()
where inSVGObj is a element, the web console says "TypeError: inSvgObj.createSVGPoint is not a function". Is it possible to create an SVG point within the to subsequently set with values representing coordinates in that 's user coordinate system?
EDIT (after considernig Robert Longson's answer):
Given that SVGPoint is created only within an "SVG root" and that I have been unable to find a way to move that to within another element, I have found more convenient to use a different svg element type: SVGMatrix. In case it helps someone (as I have spent some time trying to deal with this),It is possible to manipulate analogue values inside an SVG Point by creating an SVGMatrix that would work as a simulated point (for the purposes of coordinates. To that endthe methods .createSVGMatrix(), getCTM() and.multiply() (this last from SVGMatrix) are used. To illustrate that, I will include a (js) function that takes 4 arguments: x-coordinate in user coordinate system (ucs) to transform, y-coordinate is that ucs, object whose ucs is the want we want to transform and an object in the ucs we want to transform to; and returns am object with thrre poperties the x-coordinate in the transformed ucs, its y-coordinate and 1 (for consistency with SVG Recommendations).
function coorUcsAToUcsB(ucsAx,ucsAy,svgObjUcsA,svgObjUcsB){
var ctmUcsA=svgObjUcsA.getCTM();
var ctmUcsB=svgObjUcsB.getCTM().inverse();
var mtx=document.getElementsByTagName('svg')[0].createSVGMatrix();
mtx.e=ucsAx;
mtx.f=ucsAy;
var simulSvgP=ctmUcsB.multiply(ctmUcsA.multiply(mtx)); //1
return {"x":simulSvgP.e,"y":simulSvgP.f,"z":1};
}
//1 this line creates an svg matrix with 1st and 2nd column at 0, 3rd with coordinates of ucsB from the analogue svg matrix with coordinates in ucsA - it takes the coordinates in ucsA to viewport's cs and from there to coordinates in ucsB. For the matrix operation explanation, see this.
Any comments, in particular having overlooked a existing method that does the same or any drawbacks, will be more than welcome.
You create the SVG Point using the root element creation but once you've done that you can set whatever values in it you want. When you assign those values to an object the object will interpret them in its coordinate system.

D3 graphing selective portions of data set

I have a large time series data set I need to graph, and am trying to use D3 to do it. I plan to have my graph have the x-axis be time, and allow for movement of the graph in the x direction. I want to have the graph only load/display the points that exist in the current time range on the screen.
For example, if my dataset has times 1-100, but the graph starts out with times 1-10 shown, the graph should only graph points 1-10. Then the user may move to the right and see times 5-15 and the graph should update accordingly.
Can anyone explain to me how this might be done via d3? I am having a hard time bridging the understanding from an entire data set being loaded in at once and graphed immediately to selective graphing of subsets of the data.
I think you are looking for the selection.filter() function. For example you can have:
var allNodes = vis.selectAll("Nodes").data(data.nodes);
var validNodes = allNodes.filter(function(d){return (d.time>1 && d.time <10)});
//use normal graph functions on validNodes.
You can also apply filter directly on the array of nodes.

Categories

Resources