interactive map with leafletR variable point size - r

I create an interactive map as follows:
library(leafletR)
data(quakes)
# store data in GeoJSON file (just a subset here)
q.dat <- toGeoJSON(data=quakes[1:99,], dest=tempdir(), name="quakes")
# make style based on quake magnitude
q.style <- styleGrad(prop="mag", breaks=seq(4, 6.5, by=0.5), style.val=rev(heat.colors(5)), leg="Richter Magnitude", fill.alpha=0.7, rad=8)
# create map
q.map <- leaflet(data=q.dat, dest=tempdir(), title="Fiji Earthquakes", base.map="osm", style=q.style, popup="mag")
# view map in browser
rstudio::viewer(q.map)
Now, I want to make the size of the circle dependent on another variable. Let's say the variable 'stations'. How can I do this? If it is not possible with this package, I am open to use another package ... as long as I can put a legend, the map is interactive, a pop-up appear when clicked on and the color can depend on the value of a continuous variable.

I read through the documentation for the leafletR package, and it seems to me (and I could be wrong) that the current version doesn't support multiple styles for the same dataset. They give a few examples where they combine 2 styleSingles by listing them (e.g. style=list(sty.1, sty.2)), but that only works in conjunction with listing 2 different datasets (see P.8 in the document for more details). I tried various tricks, but none of them worked for me.
However, I came up with a hacky solution that you might want to try. After the html page is created using the leaflet() function, you can edit the Javascript code that handles the styling to make the radius property dynamic (this could also work for the other styling properties, such as fill, alpha, etc.).
What you need to know:
In the HTML document that leaflet creates, search for the definition of the style1(feature) function. You should find the following segment of code:
function style1(feature) {
return {"color": getValue(feature.properties.mag),
"fillOpacity": 0.7,
"radius": 8};
}
This function basically returns the style for each record in your dataset. As you can see, the function in its current form returns a static value for fillOpacity and radius. However, when it comes to color, it calls another function called getValue and passes it the mag (magnitude) property. If we take a look at the definition of the getValue function, we'll see that it simply defines the magnitude ranges for each color:
function getValue(x) {
return x >= 6.5 ? "#808080" :
x >= 6 ? "#FF0000" :
x >= 5.5 ? "#FF5500" :
x >= 5 ? "#FFAA00" :
x >= 4.5 ? "#FFFF00" :
x >= 4 ? "#FFFF80" :
"#808080";
}
The function definition is really simple. If x (the magnitude in this case) is greater or equal to 6.5, then the color of that data point will be "#808080". If it's between 6 and 6.5, then the color will be #FF0000". And so on and so forth.
What you can do:
Now that we see how the Javascript code handles how the colors are assigned to each data point, we can do something similar for all the other styling properties with very minimal effort. The following code segment, for instance, shows how you can make the radius dynamic based on the count of stations in the area:
/* The getValue function controls the color of the data points */
function getValue(x) {
return x >= 6.5 ? "#808080" :
x >= 6 ? "#FF0000" :
x >= 5.5 ? "#FF5500" :
x >= 5 ? "#FFAA00" :
x >= 4.5 ? "#FFFF00" :
x >= 4 ? "#FFFF80" :
"#808080";
}
/* The getRadValue function controls the radius of the data points */
function getRadValue(x) {
return x >= 100 ? 24 :
x >= 80 ? 20 :
x >= 60 ? 16 :
x >= 40 ? 12 :
8;
}
/* The updated definition of the style1 function */
function style1(feature) {
return {"color": getValue(feature.properties.mag),
"fillOpacity": 0.7,
"radius": getRadValue(feature.properties.stations)
};
}
So, with the new definition of style1(feature), now we can control both the color as well as the radius of the data points. The result of the code modification looks like this:
The good thing about this approach is that it gives you more fine-grained control over the styling properties and the range of values that they can have. The major draw-back is going to be that if you want to add a legend for those properties, then you'll have to do that manually. The logic for adding/editing the legend should be at the very bottom of the HTML document, and if you know Javascript/HTML/CSS, editing that code segment shouldn't be too difficult.
Update:
To add a legend for the new dynamic variable (in our case, the radius), you need to edit the .onAdd handler that's attached to the legend object. As I said before, the definition for this handler is usually at the bottom of the html page, and if we run the bit of code that you provided in your question, then the handler should look like this:
legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'legend');
var labels = [];
var grades = [4, 4.5, 5, 5.5, 6, 6.5];
div.innerHTML += 'Richter Magnitude<br>';
for (var i = 0; i < grades.length - 1; i++) {
div.innerHTML += '<i style="background:' + getValue(grades[i]) + '"></i> ' + grades[i] + '–' + grades[i + 1] + '<br>';
}
return div;
};
The above code simply loops through the range of values for the magnitude, and creates a box (with the appropriate color, referencing the getValue function that we looked at before) and a label. If you want to create something similar for the stations variable, let's say, we can use the same logic above. Though in this case instead of varying the color, we'll be varying the size of the circle. The following segment of code shows how to achieve that:
legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'legend');
var labels = [];
var grades = [4, 4.5, 5, 5.5, 6, 6.5];
div.innerHTML += 'Richter Magnitude<br>';
for (var i = 0; i < grades.length - 1; i++) {
div.innerHTML += '<i style="background:' + getValue(grades[i]) + '"></i> ' + grades[i] + '–' + grades[i + 1] + '<br>';
}
// Adding the range of possible of values that the variable might take
// This should be in sync with the range of values you considered in
// the getRadValue function.
var rad_grades = [40, 60, 80, 100];
// The title for this section of the legend
div.innerHTML += 'Number of stations<br>'
for (var i = 0; i < rad_grades.length - 1; i++) {
div.innerHTML += '<table style="border: none;"><tr><td class="circle" style="width: ' +
(getRadValue(rad_grades[rad_grades.length - 2]) * 2 + 6) + 'px;"><svg style="width: ' +
(getRadValue(rad_grades[i]) * 2 + 6) + 'px; height: ' + (getRadValue(rad_grades[i]) * 2 + 6) +
'px;" xmlns="http://www.w3.org/2000/svg" version="1.1"><circle cx="' + (getRadValue(rad_grades[i]) + 3) + '" cy="' +
(getRadValue(rad_grades[i]) + 3) + '" r="' + getRadValue(rad_grades[i]) + '" /></svg></td><td class="value">' +
rad_grades[i] + '–' + rad_grades[i + 1] + '</td></tr></table>';
}
return div;
};
As you can see, the type of styling property we're controlling will determine how we're specifying it in the legend. If you want to add a legend for the alpha property, for instance, then you might want to try some other approach other than using circles and controlling their width and height. The end result of the code modifications above looks like this:
Also, if you want to include the number of stations in the popup, then you'll have to edit the onEachFeature function. It's going to be the same approach we took with all the other modifications, and it's a really simple change.
The onEachFeature function looks like this in the original HTML:
function onEachFeature(feature, layer) {
if (feature.properties && feature.properties.mag) {
layer.bindPopup("mag: " + feature.properties.mag);
}
}
If you want to include the number of stations in the popup too, then you need to include it in the argument to the bindPopup method, as follows:
function onEachFeature(feature, layer) {
if (feature.properties && feature.properties.mag && feature.properties.stations) {
layer.bindPopup("mag: " + feature.properties.mag + "<br> # Stations: " + feature.properties.stations);
}
}
The end result of this change is the following:
Hope this helps.

Related

R Markdown - Highcharter - Animate graph when visible, rather on page load?

I am writing a report in R Markdown, it contains multiple animated highcharts.
The animations work fine, however they all run when the html page loads (after knitting), instead of when the user scrolls to it, so essentially the animation is pointless as the user never sees it.
An example of an animated chart is at the bottom of this question.
Is there a way to make it animate when it appears? All the examples I have found use jsfiddle and I am using R Markdown.
Many thanks
library(dplyr)
library(stringr)
library(purrr)
n <- 5
set.seed(123)
df <- data.frame(x = seq_len(n) - 1) %>%
mutate(
y = 10 + x + 10 * sin(x),
y = round(y, 1),
z = (x*y) - median(x*y),
e = 10 * abs(rnorm(length(x))) + 2,
e = round(e, 1),
low = y - e,
high = y + e,
value = y,
name = sample(fruit[str_length(fruit) <= 5], size = n),
color = rep(colors, length.out = n),
segmentColor = rep(colors2, length.out = n)
)
hcs <- c("line") %>%
map(create_hc)
hcs
Ok, I worked out how to do it myself, going to post the answer here in case someone stumbles across this post in the future.
First of all, I found NOTHING on how to do this in R.
So, I decided to do this in JS, AFTER I had knitted the R Markdown document to HTML, as it wouldn't work in R Markdown.
Once it is a HTML file, open it in TextEdit or Notepad, and add the following code just before one of the charts:
<script>
(function (H) {
var pendingRenders = [];
// https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/7557433#7557433
function isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (
window.innerHeight ||
document.documentElement.clientHeight
) &&
rect.right <= (
window.innerWidth ||
document.documentElement.clientWidth
)
);
}
H.wrap(H.Series.prototype, 'render', function deferRender(proceed) {
var series = this,
renderTo = this.chart.container.parentNode;
// It is appeared, render it
if (isElementInViewport(renderTo) || !series.options.animation) {
proceed.call(series);
// It is not appeared, halt renering until appear
} else {
pendingRenders.push({
element: renderTo,
appear: function () {
proceed.call(series);
}
});
}
});
function recalculate() {
pendingRenders.forEach(function (item) {
if (isElementInViewport(item.element)) {
item.appear();
H.erase(pendingRenders, item);
}
});
}
if (window.addEventListener) {
['DOMContentLoaded', 'load', 'scroll', 'resize']
.forEach(function (eventType) {
addEventListener(eventType, recalculate, false);
});
}
}(Highcharts));
</script>
The charts then animate when you scroll to them, rather than when you open the HTML file.
Note: The JSFIDDLE I got the code from was from here:
https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/studies/appear/

d3 map - After using blur filter, zoom does not work properly

I am using the blur effect on the d3 map as given here: http://geoexamples.blogspot.in/2014/01/d3-map-styling-tutorial-ii-giving-style.html?
But after using this method (because of how the data is loaded..using datum) my zoom functionality behaves randomly. Irrespective of where I click it zooms to the same point. Also, the animations have become very slow after using the filter.
Is there any other way to achieve blur? Or a solution to this problem?
Any help?
Thanks.
This is the code for the world creation in case when filtering is required (use of datum as per the code on the above site).
d3.json("world-110m2.json", function(error, world) {
g.insert("path")
.datum(topojson.feature(world, world.objects.land))
.attr("d", path);
g.insert("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("d", path)
.append("path");
g.selectAll("path")
.on("click", click);})
This is the code used in case filtering is not required (No use of datum - maybe the datum is causing the issue)
d3.json("world-110m2.json", function(error,topology) {
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d",path)
.on("click", click);)}
This is the zoom function: got the code from here: http://bl.ocks.org/mbostock/2206590
function click(d) {
var x, y, k;
var centered;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
if (active === d) return reset();
g.selectAll(".active").classed("active", false);
d3.select(this).classed("active", active = d);
var b = path.bounds(d);
g.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
g.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
The blur filter consumes lots of resources, as indicated in the post. Speciallly if you combine it with other filters.
One solution would be using Canvas instead of SVG. Here you have some filters using the Canvas element. It should be possible to achieve the same result.
I can't find why the zoom stops working, but the performance is slower because you use all the data, so you are applying the filter to all the data instead of using only the part of the word you are showing, so you are using a much bigger image when you zoom.

Sampling points in a vector

I have a vector _pts that contains values for (x,y,z), i.e. a point in 3D. Starting with _pts[0], I want to select those points whose distance between previously selected points is bigger than sampleRadius.
Here is my code but apparently something's wrong because is selecting a lot of points instead of just selecting a few.
Can anyone see what am I doing wrong? Probably you would need more code to see what can be missed, but I would also appreciate any idea on how can I implement this.
float distance;
bool distanceIsOk;
//PICK A POINT IN VECTOR _pts
for (int cPIdx = 0; cPIdx < _pts.size(); cPIdx++) {
distanceIsOk = true;
//CHECK DISTANCE AGAINST PREVIOUSLY PICKED POINTS
for (int dPIdx = 0; dPIdx < indeces.size(); dPIdx++) {
distance = sqrt(
(_pts[cPIdx].v[0] - _pts[dPIdx].v[0])*(_pts[cPIdx].v[0] - _pts[dPIdx].v[0]) +
(_pts[cPIdx].v[1] - _pts[dPIdx].v[1])*(_pts[cPIdx].v[1] - _pts[dPIdx].v[1]) +
(_pts[cPIdx].v[2] - _pts[dPIdx].v[2])*(_pts[cPIdx].v[2] - _pts[dPIdx].v[2])
);
//IF DISTANCE IS <= SUBSAMPLERADIUS FOR AT LEAST ONE PREVIOUSLY SELECTED POINT
if (distance <= subsampleRadius) {
//DISCARD THE POINT
distanceIsOk = false;
dPIdx += indeces.size();
}
}
//OTHERWISE INCLUDE THAT POINT
if (distanceIsOk == true) {
indeces.push_back(cPIdx);
}
}
Found the error. Instead of
distance = sqrt(
(_pts[cPIdx].v[0] - _pts[dPIdx].v[0])*(_pts[cPIdx].v[0] - _pts[dPIdx].v[0]) +
(_pts[cPIdx].v[1] - _pts[dPIdx].v[1])*(_pts[cPIdx].v[1] - _pts[dPIdx].v[1]) +
(_pts[cPIdx].v[2] - _pts[dPIdx].v[2])*(_pts[cPIdx].v[2] - _pts[dPIdx].v[2])
it should be
distance = sqrt(
(_pts[cPIdx].v[0] - _pts[indeces[dPIdx]].v[0])*(_pts[cPIdx].v[0] - _pts[indeces[dPIdx]].v[0]) +
(_pts[cPIdx].v[1] - _pts[indeces[dPIdx]].v[1])*(_pts[cPIdx].v[1] - _pts[indeces[dPIdx]].v[1]) +
(_pts[cPIdx].v[2] - _pts[indeces[dPIdx]].v[2])*(_pts[cPIdx].v[2] - _pts[indeces[dPIdx]].v[2])

Simple grid draw doesn't work

I'm trying to draw a simple grid on a canvas. First I did this
function start()
{
var x = 0;
var y = 0;
for (x = 0; x < 500; x += 50)
{
line(0 + x, 50 + y, 50 + x, 50 + y, 1, "#111");
line(50 + x, 0 + y, 50 + x, 50 + y, 1, "#111");
if (x == 450)
{
x = -50;
y += 50;
}
if (y == 500)
{
x = 500;
}
}
}
It works fine. But I want to be able to easily change the size of the grid and canvas. So I did this:
function start()
{
var x = 0;
var y = 0;
var cW = canvas.width;
var cH = canvas.hight;
var gS = 50; //gS = gridSpace
for (x = 0; x < cW; x += gS)
{
line(0 + x, gS + y, gS + x, gS + y, 1, "#111");
line(gS + x, 0 + y, gS + x, gS + y, 1, "#111");
if (x == cW - gS)
{
x = -gS;
y += gS;
}
if (y == cH)
{
x = cW;
}
}
}
It does not work! Please help me.
PS. I'm using a library.
`
You might want to approach this a little differently. I'm not entirely sure of what you're trying to accomplish here, but here are some pointers/questions that may guide you toward what you're trying to do:
When drawing a grid, you are drawing a series of horizontal lines and a series of vertical lines. Use two loops to simplify that process.
For the horizontal lines, the y-value varies, but the x-coords for the line endpoints stay the same (e.g. 0 and cW). The converse applies to the vertical lines.
What kind of spacing are you really trying to achieve? Typically you're looking at either dividing the space into a certain number of areas (say 6 rows and 4 columns), or spacing that doesn't adapt to the specific canvas you're drawing on (this is what you're code seems to be trying to do). So, the first will adapt to the size of the canvas while the latter will just display more/fewer rows/columns as the canvas size varies.
I hope that helps you solve your problem, please let me know if you need any more help!
One possibility, is that you have height spelled incorrectly. I believe javascript, if this is javascript, won't complain about incorrectly named variables. (I could be wrong on that).
var cH = canvas.hight;
should be
var cH = canvas.height;

Tournament Brackets algorithm

I need to create an asp.net page that auto generate a brackets tournament tennis style.
Regarding the managing of match in database, it's not a problem.
The problem is the dynamic graphics creation of brackets.
The user will be able to create tournament by 2-4...32 players.
And i don't know ho to create the graphics bracket in html or gdi...
Using Silverlight, and a Grid, You can produce something like this:
To do it, define a regular UserControl containing a Grid. (This is the default when you build a silverlight app in VS2008 with the Silverlight 3.0 SDK).
Then, add a call to the following in the constructor for the user control:
private void SetupBracket(int n)
{
var black = new SolidColorBrush(Colors.Gray);
// number of levels, or rounds, in the single-elim tourney
int levels = (int)Math.Log(n, 2) + 1;
// number of columns in the Grid. There's a "connector"
// column between round n and round n+1.
int nColumns = levels * 2 - 1;
// add the necessary columns to the grid
var cdc = LayoutRoot.ColumnDefinitions;
for (int i = 0; i < nColumns; i++)
{
var cd = new ColumnDefinition();
// the width of the connector is half that of the regular columns
int width = ((i % 2) == 1) ? 1 : 2;
cd.Width = new GridLength(width, GridUnitType.Star);
cdc.Add(cd);
}
var rdc = LayoutRoot.RowDefinitions;
// in the grid, there is one row for each player, and
// an interleaving row between each pair of players.
int totalSlots = 2 * n - 1;
for (int i = 0; i < totalSlots; i++)
{
rdc.Add(new RowDefinition());
}
// Now we have a grid of the proper geometry.
// Next: fill it.
List<int> slots = new List<int>();
ImageBrush brush = new ImageBrush();
brush.ImageSource = new BitmapImage(new Uri("Bridge.png", UriKind.Relative));
// one loop for each level, or "round" in the tourney.
for (int j = 0; j < levels; j++)
{
// Figure the number of players in the current round.
// Since we insert the rounds in the reverse order,
// think of j as the "number of rounds remaining."
// Therefore, when j==0, playersThisRound=1.
// When j == 1, playersThisRound = 2. etc.
int playersThisRound = (int)Math.Pow(2, j);
int x = levels - j;
int f = (int)Math.Pow(2, x - 1);
for (int i = 0; i < playersThisRound; i++)
{
// do this in reverse order. The innermost round is
// inserted first.
var r = new TextBox();
r.Background = black;
if (j == levels - 1)
r.Text = "player " + (i + 1).ToString();
else
r.Text = "player ??";
// for j == 0, this is the last column in the grid.
// for j == levels-1, this is the first column.
// The grid column is not the same as the current
// round, because of the columns used for the
// interleaved connectors.
int k = 2 * (x - 1);
r.SetValue(Grid.ColumnProperty, k);
int m = (i * 2 + 1) * f - 1;
r.SetValue(Grid.RowProperty, m);
LayoutRoot.Children.Add(r);
// are we not on the last round?
if (j > 0)
{
slots.Add(m);
// Have we just inserted two rows? Then we need
// a connector between these two and the next
// round (the round previously added).
if (slots.Count == 2)
{
string xamlTriangle = "<Path xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' "+
"xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' " +
"Data='M0,0 L 100 50 0 100 Z' Fill='LightBlue' Stretch='Fill'/>";
Path path = (Path)System.Windows.Markup.XamlReader.Load(xamlTriangle);
path.SetValue(Grid.ColumnProperty, 2 * (x - 1) + 1);
path.SetValue(Grid.RowProperty, slots[0]);
path.SetValue(Grid.RowSpanProperty, slots[1] - slots[0] + 1);
this.LayoutRoot.Children.Add(path);
slots.Clear();
}
}
}
}
}
In the above, the connector is just an isosceles triangle, with the apex pointing to the right. It is generated by XamlReader.Load() on a string.
You would also want to pretty it up, style it with different colors and fonts, I guess.
You can insert this silverlight "user control" into any HTML web page, something like embedding a flash app into a page. There are silverlight plugins for IE, Firefox, Opera, Safari, and Chrome.
If you don't want to use Silverlight, you could use a similar approach to construct an HTML table.

Resources