intro
I am pretty agile in R, but my java skills are non-existent. Therefor I'm throwing myself at your mercy to answer this, hopefully not too complex, question (or I will have a hard time figuring out the answers ;-)).
Running the code below requires you to download three leaflet plugins from github (links in the comments inside the code). They should be placed in a folder ./script, relative to where you are running the code.
sample data
I have excel-sheets with multiple routes. For sake of simplicity, I already read in a file using the following code, so I do not have to share the excel-file online:
# read excel file
bestand <- "./data/CBM_Schuttorf_Buren.xlsx"
bladen <- readxl::excel_sheets(bestand)
xldata <- lapply(bladen, function(x) {
readxl::read_excel(path = bestand, sheet = x,
col_types = c(rep(c("numeric", "text"), 2), rep("numeric", 2)))
})
names(xldata) <- bladen
This results in the following object, which you will need to continue the code with
bladen <- c("A1L", "A1R")
xldata <- list(A1L = structure(list(route = c(1, 1, 2, 2, 2, 3, 3, 3),
routeType = c("stremming", "stremming", "omleiding", "omleiding",
"omleiding", "omleiding", "omleiding", "omleiding"), punt = c(1,
2, 1, 2, 3, 1, 2, 3), puntType = c("start", "eind", "start",
"via", "eind", "start", "via", "eind"), lat = c(52.341823,
52.284989, 52.340234, 52.193045, 52.302415, 52.349596, 52.193045,
52.302415), lon = c(7.254037, 6.74575, 7.271095, 7.102321,
6.715246, 7.258845, 7.102321, 6.715246)), class = c("tbl_df",
"tbl", "data.frame"), row.names = c(NA, -8L)), A1R = structure(list(
route = c(1, 1, 2, 2, 2, 3, 3, 3), routeType = c("stremming",
"stremming", "omleiding", "omleiding", "omleiding", "omleiding",
"omleiding", "omleiding"), punt = c(1, 2, 1, 2, 3, 1, 2,
3), puntType = c("start", "eind", "start", "via", "eind",
"start", "via", "eind"), lat = c(52.284267, 52.341886, 52.303024,
52.19279, 52.354846, 52.303024, 52.19279, 52.339145), lon = c(6.754951,
7.251379, 6.713831, 7.104181, 7.258402, 6.713831, 7.104181,
7.285606)), class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA,
-8L)))
my problem
The sample data is a simplified issue. There are only two list entries, A1L and A1R. In my production data there will be many more entries.
What i want as a result, is a dynamic version of the code below. Here I hardcoded the Layergroups A1L and A1R everywhere. ButWhile this works as a proof-of-concept, this is not workable in production.
As mentioned before, I need the functionality of several leaflet pluging, so I relied heavily on the htmlwidgets::onRender()- funciton to get down what I need. This is also my Achilles-heel, since I am a complete n00b in javascript.
desired output
I am open for all suggestions that can replicate the results of the code below, without hardcoding the filtering/Layergroups..
note: the arrows on the end of the polyline only show when the leaflet is shown in the browser. They do not show inside the rstudio viewer (took me some frustration to find that one out ;-) )
click here for my output
my code
library(tidyverse)
library(readxl)
library(osrm)
library(leaflet)
library(geojsonsf)
# below commented out, xldata is already provided
# # read excel file
# bestand <- "./data/myfile.xlsx"
# bladen <- readxl::excel_sheets(bestand)
# xldata <- lapply(bladen, function(x) {
# readxl::read_excel(path = bestand, sheet = x,
# col_types = c(rep(c("numeric", "text"), 2), rep("numeric", 2)))
# })
# names(xldata) <- bladen
# split individual routes (will become polylines later on)
routes <- lapply(xldata, function(x) split(x, f = x$route))
# create real routes, using osm routing
df <-
dplyr::bind_rows(
lapply(seq.int(routes), function(i) {
dplyr::bind_rows(
lapply(seq.int(lengths(routes)[i]), function(j) {
temp <- osrmRoute(loc = as.data.frame(routes[[i]][[j]][, c("lon", "lat")]),
overview = "full", returnclass = "sf") %>%
mutate(naam = paste0(bladen[i], "_", routes[[i]][[j]][1,2], routes[[i]][[j]][1,1])) %>%
mutate(groep = bladen[i]) %>%
mutate(groepVol = paste0("groups.",bladen[i])) %>%
mutate(type = ifelse(grepl("stremming", naam), "stremming", "omleiding"))
}))
})
)
df
# get boundaries for map
grens <- sf::st_bbox(df) %>% as.vector()
# create named list of geojson routes
plotdata <- lapply(split(df, f = df$naam), sf_geojson)
# PLUGIN SECTION
# from: https://github.com/slutske22/leaflet-arrowheads
arrowHead <- htmlDependency(
"leaflet-arrowheads",
"0.1.2",
src = normalizePath(".\\script"),
#src = "./script",
script = "leaflet-arrowheads.js"
)
# from https://github.com/makinacorpus/Leaflet.GeometryUtil
geometryutil <- htmlDependency(
"leaflet.geometryutil",
"0.1.2",
src = normalizePath(".\\script"),
#src = "./script",
script = "leaflet.geometryutil.js"
)
registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}
# plot the map and layers
leaflet() %>%
#register plugins
registerPlugin(arrowHead) %>%
registerPlugin(geometryutil) %>%
# add basemap
addProviderTiles(providers$CartoDB.Positron) %>%
# set map boundaries
fitBounds( grens[1], grens[2], grens[3], grens[4]) %>%
onRender("function(el, x, data) {
// funciton to define line color based on
// feature.properties.type
function getColor(d) {
return d == 'stremming' ? 'red' :
d == 'omleiding' ? 'seagreen' :
'black';
}
// funciton to define line dash based on
// feature.properties.type
function getDash(d) {
return d == 'stremming' ? '20' :
d == 'omleiding' ? '' :
'';
}
// function to set style of polylines
function newstyle(feature) {
return {
color: getColor(feature.properties.type),
weight: 10,
opacity: 1,
dashArray: getDash(feature.properties.type),
fillOpacity: 0.7
};
}
///////////////////////////////////////
//would like to make the code below this dynamic
//based on the groep-property in the JSON object
//so A1L and A1R groups (and thereby the filtering)
//are read in directly from the data object df
///////////////////////////////////////
// filtering
function A1L(feature) {if (feature.properties.groep === 'A1L') return true}
function A1R(feature) {if (feature.properties.groep === 'A1R') return true}
// crteation of layergroups
var groups = {
A1L: new L.LayerGroup(),
A1R: new L.LayerGroup()
};
// create layers and add to groups
var A1L = L.geoJSON(data, {
filter: A1L,
style: newstyle,
arrowheads: {frequency: 'endonly', yawn: 45, size: '30px', fill: true}
})
.on('mouseover', function (e) {e.target.setStyle({weight: 15, opacity: 1 });})
.on('mouseout', function (e) {e.target.setStyle({weight: 10, opacity: 0.75});})
.addTo(groups.A1L);
var A1R = L.geoJSON(data, {
filter: A1R,
style: newstyle,
arrowheads: {frequency: 'endonly', yawn: 45, size: '30px', fill: true}
})
.on('mouseover', function (e) {e.target.setStyle({weight: 15, opacity: 1 });})
.on('mouseout', function (e) {e.target.setStyle({weight: 10, opacity: 0.75});})
.addTo(groups.A1R);
var baseLayers = {
'A1L': A1L,
'A1R': A1R
};
var layerControl = L.control.layers(baseLayers, null, {collapsed: false}).addTo(this);
baseLayers['A1L'].addTo(this);
}", data = sf_geojson(df))
what I have tried so far
I found something that might be the solution here, but I lack the java skills to:
see if this is indeed the way to go, and if so:
how can this be implementd inside my code.
After >1 day of fruitless effort, I finally found an answer I could work with.
It is now working as desired, here is my answer for anyone looking for this possibility (and I found some in the past 24 h ;-) )
# plot the map and layers
leaflet() %>%
#register plugins
registerPlugin(arrowHead) %>%
registerPlugin(geometryutil) %>%
registerPlugin(groupedlayercontrol) %>%
# add basemap
addProviderTiles(providers$CartoDB.Positron) %>%
# set map boundaries
fitBounds( grens[1], grens[2], grens[3], grens[4]) %>%
onRender("function(el, x, data) {
// read data from the named list passd to onRender
// data.name_from_list
var routes = data.routes;
var groups = data.groups;
var types = groups;
// function to define line color based on
// feature.properties.type
function getColor(d) {
return d == 'stremming' ? 'red' :
d == 'omleiding' ? 'seagreen' :
'black';
}
// funciton to define line dash based on
// feature.properties.type
function getDash(d) {
return d == 'stremming' ? '20' :
d == 'omleiding' ? '' :
'';
}
// function to set style of polylines
function newstyle(feature) {
return {
color: getColor(feature.properties.type),
weight: 10,
opacity: 1,
dashArray: getDash(feature.properties.type),
fillOpacity: 0.7
};
}
// layerControl optioesn for groupedOverlays
var options = {
exclusiveGroups: ['Stremming'],
groupCheckboxes: false,
collapsed: false
};
// add empty layercontrol
var layerControl = L.control.groupedLayers(null, null, options).addTo(this);
// iterate over types, filter by that type, and format the layer for that feature type
types.forEach(function(type) {
var layer = L.geoJson(routes, {
filter: function(feature, layer) {
return feature.properties.groep == type;
},
style: newstyle,
arrowheads: {frequency: 'endonly', yawn: 45, size: '30px', fill: true}
})
.on('mouseover', function (e) {e.target.setStyle({weight: 15, opacity: 1 });})
.on('mouseout', function (e) {e.target.setStyle({weight: 10, opacity: 0.75});})
// all done with the layer, add it to the control
layerControl.addOverlay(layer, type, 'Stremming');
});
}", data = list(routes = sf_geojson(df), groups = bladen))
Related
I'm trying to format two series of my graph in highchart. The first graph is a serie and the another is a %change. So I want to format each serie using "hc_tooltip" argument. A simplified version of my code to show my problem is the next:
a <- c(30, 40, 10, 40, 80)
b <- c(3, 4, -1, -4, -8)
d<-cbind(a,b)
dt <- seq(as.Date("2018-01-01"), as.Date("2018-01-05"), by = "days")
ts <- xts(d, dt )
highchart(type="stock") %>%
hc_add_series(ts$a,
type = "line",
color="black") %>%
hc_add_series(ts$b,
type = "lollipop",
color="red") %>%
hc_tooltip(pointFormat = '<b>{point.a.name}</b>
{point.y.a:.4f}')%>%
hc_tooltip(pointFormat = '<b>{point.b.name}</b>
{point.y.b:.4f}%')
Like I hoped, It's not working. I want I can see the data from the first serie like integer and the second like % in the graph when I put the mouse in the serie. How can I achieve that?
To achieve that, you need to use the tooltip.formatter with the relevant function
Example:
hc_tooltip(formatter = JS("function () {
if (this.series.name === "A") {
return `<b>${this.series.name}</b></br>${this.y}`
} else if (this.series.name === "B") {
return `<b>${this.series.name}</b></br>${this.y}%`
}}")
JS Demo:
https://jsfiddle.net/BlackLabel/zqyp85ag/
API Reference:
https://api.highcharts.com/highcharts/tooltip.formatter
I am trying to create a time line plot using leaflet and leaftime packages. I want to set custom color in addTimeline to specify each point to his groups, as follows:
library(leaflet)
library(leaftime)
library(geojsonio)
power_d <- data.frame(
"Latitude" = c(
33.515556, 38.060556, 47.903056, 49.71, 49.041667, 31.934167,
54.140586, 54.140586, 48.494444, 48.494444
),
"Longitude" = c(
129.837222, -77.789444, 7.563056, 8.415278, 9.175, -82.343889,
13.664422, 13.664422, 17.681944, 17.681944
),
"start" = seq.Date(as.Date("2015-01-01"), by = "day", length.out = 10),
"end" = seq.Date(as.Date("2015-01-01"), by = "day", length.out = 10) + 1,
color_temp=rep(c("red","blue","green"),len=10)
)
power_geo <- geojsonio::geojson_json(power_d ,lat="Latitude",lon="Longitude")
leaflet() %>%
addTiles() %>%
setView(44.0665,23.74667,2) %>%
addTimeline(data = power_geo,
timelineOpts = timelineOptions(
styleOptions = styleOptions(
radius = 5,
color=color_temp,
fillColor = color_temp,
fillOpacity = 1
)
)
)
Unfortunately I got following error:
Error in lapply(x, f) : object 'color_temp' not found
I also try replacing color_temp with power_d$color_temp, the code run without error, but the color of points do not change. The color arguments not work in above code, why? and how to fix it?
It doesn't seem as if you can pass a vector of colours with the standard styleOptions, however, an example from the help page for ?addTimeline show how you can add colours based on the data using a little JavaScript (which thankfully is provided in the example).
Using the example that starts "# to style each point differently based on the data" you need to change it slightly to point to your colour vector e.g. change data.properties.color to data.properties.color_temp. Running the code below leads to
# code
leaflet(power_geo) %>%
addTiles() %>%
setView(44.0665,23.74667,2) %>%
addTimeline(
timelineOpts = timelineOptions(
styleOptions = NULL,
pointToLayer = htmlwidgets::JS(
"
function(data, latlng) {
return L.circleMarker(
latlng,
{
radius: 25,
color: data.properties.color_temp,
fillColor: data.properties.color_temp,
fillOpacity: 1
}
);
}
"
)
)
)
I'm trying to implement the plugin extension , groupedLayerControl, for leaflet maps. You can see below the code I'm working with (currently not working). I'm trying to put the overlayers in different group categories. I'm new to javascript so I really can't identify what I'm doing wrong. The code does not give me the groups' layers in the map. Can someone help me out?
registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}
Value1 = c(12,43,54,34,23,77,44,22)
Value2 = c(6,5,2,7,5,6,4,3)
Lat = c(51.1, 51.6, 57.3, 52.4, 56.3, 54.3, 60.4, 49.2)
Lon = c(5, -3, -2, -1, 4, 3, -5, 0)
leaflet() %>%
addProviderTiles(providers$CartoDB.Positron, group = "Alpha") %>%
registerPlugin(groupedLayerControlPlugin) %>%
onRender("function(el, x) {
var baseLayers = {
'Alpha': this.layerManager.getLayerGroup('Alpha'),
};
var groupedOverlays = {
'Group 1': {
'Value1': this.layerManager.getLayerGroup('Value1'),
},
'Group 2': {
'Value2': this.layerManager.getLayerGroup('Value2'),
}
};
var Options = {
groupCheckboxes: true
};
console.log(L.control.groupedLayers);
L.control.groupedLayers(baseLayers, groupedOverlays, Options).addTo(this);
}") %>%
addCircles(lat=Lat, lng=Lon,
radius = Value1*1000, group = "Value1") %>%
addCircles(lat=Lat, lng=Lon,
radius = Value2*10000, group = "Value1")
This is an old question , but thought I'd add a complete working example using your data. You only had one issue in the provided example, which was that you needed at least one group = "Value2" argument for one of the leaflet::addCircles(). Both of the addCircles() in your example show group = "Value1").
In the below code, I've also added an additional base layer to demonstrate base layer grouping in addition to the groupedlayercontrol of overlays.
library(dplyr)
library(leaflet)
library(htmlwidgets)
Value1 = c(12,43,54,34,23,77,44,22)
Value2 = c(6,5,2,7,5,6,4,3)
Lat = c(51.1, 51.6, 57.3, 52.4, 56.3, 54.3, 60.4, 49.2)
Lon = c(5, -3, -2, -1, 4, 3, -5, 0)
urlf <- 'https://raw.githubusercontent.com/ismyrnow/leaflet-groupedlayercontrol/gh-pages/dist/%s'
download.file(sprintf(urlf,'leaflet.groupedlayercontrol.min.js'), 'C:/Temp/L.Control.groupedlayer.js', mode="wb")
download.file(sprintf(urlf,'leaflet.groupedlayercontrol.min.css'), 'C:/Temp/L.Control.groupedlayer.css', mode="wb")
groupedLayerControlPlugin <- htmltools::htmlDependency(
name = 'ctrlGrouped',
version = "1.0.0",
src = c(file = normalizePath('C:/Temp')),
script = "L.Control.groupedlayer.js",
stylesheet = "L.Control.groupedlayer.css"
)
registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}
leaflet() %>%
addProviderTiles("Stamen.Watercolor", group = "Beta") %>%
addProviderTiles(providers$CartoDB.Positron, group = "Alpha") %>%
registerPlugin(groupedLayerControlPlugin) %>%
onRender("function(el, x) {
var groups = {
Value1: this.layerManager.getLayerGroup('Value1'),
Value2: this.layerManager.getLayerGroup('Value2')
};
var baseLayers = {
'Alpha': this.layerManager.getLayerGroup('Alpha'),
'Beta': this.layerManager.getLayerGroup('Beta')
};
var groupedOverlays = {
'all value groups': {
'Value 1' : groups.Value1,
'Value 2' : groups.Value2}
};
var Options = {
groupCheckboxes: true
};
console.log(L.control.groupedLayers);
L.control.groupedLayers(baseLayers, groupedOverlays, Options).addTo(this);
}") %>% addCircles(lat=Lat, lng=Lon,
radius = Value1*1000, group = "Value1") %>%
addCircles(lat=Lat, lng=Lon,
radius = Value2*10000, group = "Value2")
I make a leaflet map with a responsive heatmap using addHeatmap. Unfortunately, this kind of tool it is not enough useful because two main problems: 1) The heatmap is redrawed with each new level of zoom and 2) you can not make the heatmap and the points in a separated group each one.
It is possible a similar solution with addWebGLHeatmap?
There is the code for the addHeatmap solution, following this question
library(crosstalk)
library(leaflet)
library(leaflet.extras)
library(dplyr)
# Wrap data frame in SharedData
sd <- SharedData$new(quakes[sample(nrow(quakes), 10),])
bscols(widths=c(3,9),
# Create a filter input
filter_slider("mag", "Magnitude", sd, column=~mag, step=0.1),
leaflet(sd) %>%
addTiles() %>%
addMarkers() %>%
addHeatmap(layerId="heatmap") %>%
removeHeatmap("heatmap") %>%
htmlwidgets::onRender("
function(el,x){
var myMap = this;
var coord_state;
var coords;
function get_markers(){
coord_state = [];
myMap.eachLayer(function(layer){
if (typeof layer.options.lat != 'undefined'){
coord_state.push([layer.options.lat, layer.options.lng, 0.5]);
}
})
return(coord_state)
}
function update_layer(){
coords = get_markers()
heat1.setLatLngs(coords);
heat1.redraw();
}
var heat1 = L.heatLayer(get_markers(), {radius: 25}).addTo(myMap);
myMap.on('layerremove', update_layer);
myMap.on('layeradd', update_layer);
}
"))
This method is kind of a hack, but still should be able to work with addWebGLHeatmap. It adds two sets of identical markers and hides one which controls the heatmap. This allows for the layer control. A working example can be found here:
https://rpubs.com/Jumble/leaflet_webgl_heatmap
Below is the code that produced this. This code solves the main two problems although it struggles if you wan't to plot over 1000 points.
Rather than using crosstalk it might be better to use a combination of something like leafgl, shiny and addWebGLHeatmap if you are wanting to plot thousands of points.
n <- 200
data <- data.frame(id = seq(1, n*2), lat = rnorm(n, 0, 15), long = rnorm(n, 0, 15), group=c(rep("Heatmap",n), rep("Markers", n)), mag=rep(as.integer(runif(n, 0, 20)),2))
sd <- SharedData$new(data)
bscols(widths=c(3,9),
filter_slider("mag", "Magnitude", sd, column=~mag, step=0.1),
leaflet(sd, options=leafletOptions(preferCanvas = TRUE)) %>%
addTiles() %>%
leaflet::setView(lat=0, lng=0, zoom=4) %>%
addMarkers(group=~group) %>%
leaflet.extras::addWebGLHeatmap(layerId = "heatmapwebgl") %>%
leaflet.extras::removeWebGLHeatmap("heatmapwebgl") %>%
addLayersControl(overlayGroups = c("Heatmap", "Markers"), options=layersControlOptions(collapsed=FALSE)) %>%
htmlwidgets::onRender("
function(el,x){
var myMap = this;
var coord_state;
// hide heatmap markers
setTimeout(function(){
myMap.eachLayer(function(layer){
if (layer.options.group=='Heatmap'){
layer.setOpacity(0);
layer.getElement().style.pointerEvents = 'none';
}
})
}, 100)
function get_markers(){
coord_state = [];
myMap.eachLayer(function(layer){
if (layer.options.group=='Heatmap'){
coord_state.push([layer.options.lat, layer.options.lng, 0.5]);
layer.getElement().style.pointerEvents = 'none';
}
})
return(coord_state)
}
function redraw_heatmap(){
heatmap.setData(get_markers());
}
var heatmap = L.webGLHeatmap({size: 1000000,units: 'm',alphaRange: 1});
heatmap.setData(get_markers());
myMap.addLayer(heatmap);
myMap.on('layerremove', redraw_heatmap);
myMap.on('layeradd', redraw_heatmap);
}
"))
Below for Circle Markers
n <- 200
data <- data.frame(id = seq(1, n*2), lat = rnorm(n, 0, 15), long = rnorm(n, 0, 15), group=c(rep("Heatmap",n), rep("Markers", n)), mag=rep(as.integer(runif(n, 0, 20)),2))
sd <- SharedData$new(data)
bscols(widths=c(3,9),
filter_slider("mag", "Magnitude", sd, column=~mag, step=0.1),
leaflet(sd) %>%
addTiles() %>%
leaflet::setView(lat=0, lng=0, zoom=4) %>%
addCircleMarkers(group=~group, opacity=~ifelse(group=="Heatmap", 0, 0.5), fillOpacity=~ifelse(group=="Heatmap", 0, 0.2)) %>%
leaflet.extras::addWebGLHeatmap(layerId = "heatmapwebgl") %>%
leaflet.extras::removeWebGLHeatmap("heatmapwebgl") %>%
addLayersControl(overlayGroups = c("Heatmap", "Markers"), options=layersControlOptions(collapsed=FALSE)) %>%
htmlwidgets::onRender("
function(el,x){
var myMap = this;
var coord_state;
function get_markers(){
coord_state = [];
myMap.eachLayer(function(layer){
if (layer.options.group=='Heatmap'){
coord_state.push([layer.options.lat, layer.options.lng, 0.5]);
layer.getElement().style.pointerEvents = 'none';
}
})
return(coord_state)
}
function redraw_heatmap(){
heatmap.setData(get_markers());
}
var heatmap = L.webGLHeatmap({size: 1000000,units: 'm',alphaRange: 1});
heatmap.setData(get_markers());
myMap.addLayer(heatmap);
myMap.on('layerremove', redraw_heatmap);
myMap.on('layeradd', redraw_heatmap);
}
"))
I would like to create a bar and line chart using dygraphs, which seems like it should be possible based on the "Bar & Line Chart" dygraphs example here, and the dyBarChart() custom plotter provided in the dygraphs package.
Using the custom wrapper, I can create a barplot, so I think that code is working:
library(dygraphs)
dyBarChart <- function(dygraph) {
dyPlotter(
dygraph = dygraph,
name = "BarChart",
path = system.file("examples/plotters/barchart.js",package = "dygraphs")
)
}
lungDeaths <- cbind(ldeaths, mdeaths)
dygraph(lungDeaths) %>%
dyBarChart()
I assumed that I could then use dySeries() to customize the series I wanted to show up with a line/bar, but neither of the following work. They do not error out, but nothing is created. I'm also not sure if the "linePlotter" is the correct plotter name, but either way, I need a little help.
# doesn't work
dygraph(lungDeaths) %>%
dyBarChart() %>%
dySeries("ldeaths", plotter = "linePlotter")
# also doesn't work:
dygraph(lungDeaths) %>%
dySeries("ldeaths", plotter = "dyBarChart") %>%
dySeries("mdeaths", color = "blue")
Thanks.
Sometimes you get lucky… I‘ve worked on the same thing a couple of weeks ago and I‘ve found that the documentation is not quite clear on how to do it. But you were pretty close yourself.
How to do it – step by step:
You have to set the plotter for each dyseries
The plotter argument in the dyseries command does not take functions names. But it needs to be a javascript function as plain text
Stacking the bars is easier. Multibars need a way to pass an argument to the javascript function, which you cannot do directly in the package. So I had to do a workaround (At least I found no better way to do it in R).
BTW, setting the dyPlotter command did not work because it sets the plotter globally for all dySeries in the plot. At least that‘s what I figure it does.
So without further ado, here‘s my code. I have added some more test data just to show all the functions.
Test data:
library(xts)
library(dygraphs)
test<-xts(matrix(rnorm(100*4), ncol=4, nrow=100), order.by=seq.POSIXt(as.POSIXct("2017-01-01 00:00", tz="UTC"),by=3600, length.out = 100))
colnames(test)<-c("Series_A","Series_B", "Series_C", "Series_D")
Functions:
dy_position<-function(data_final, plot_title, y2_names=NULL, y1_label, y2_label, y1_step=F, y2_step=F, stacked=T){
data_final<-reorder_xts(data_final, y2_names) #reorder necessary so that all y2 are at the right end of the xts. Needed for the multibar plot
dyg <- dygraphs::dygraph(data_final, main=plot_title)
dyg <- dygraphs::dyAxis(dyg, "x", rangePad=20)
dyg <- dygraphs::dyAxis(dyg, "y", label = y1_label,
axisLabelWidth = 90)
y1_names<-colnames(data_final)[!(colnames(data_final) %in%y2_names)]
if (length(y1_names)==1){
stacked<-T #in this case only stacking works
}
if (stacked){
dyg <- dygraphs::dyOptions(dyg,stepPlot=y1_step,stackedGraph = T)
for(i in seq_along(y1_names)) {
dyg <- dygraphs::dySeries(dyg, y1_names[i], axis = "y", strokeWidth = 1.5, stepPlot = y1_step, plotter=" function barChartPlotter(e) {
var ctx = e.drawingContext;
var points = e.points;
var y_bottom = e.dygraph.toDomYCoord(0);
ctx.fillStyle = e.color;
// Find the minimum separation between x-values.
// This determines the bar width.
var min_sep = Infinity;
for (var i = 1; i < points.length; i++) {
var sep = points[i].canvasx - points[i - 1].canvasx;
if (sep < min_sep) min_sep = sep;
}
var bar_width = Math.floor(2.0 / 3 * min_sep);
// Do the actual plotting.
for (var i = 0; i < points.length; i++) {
var p = points[i];
var center_x = p.canvasx;
ctx.fillRect(center_x - bar_width / 2, p.canvasy,
bar_width, y_bottom - p.canvasy);
ctx.strokeRect(center_x - bar_width / 2, p.canvasy,
bar_width, y_bottom - p.canvasy);
}
}")
}
} else {
dyg <- dygraphs::dyOptions(dyg,stepPlot=y1_step)
for(i in seq_along(y1_names)) {
#plotter in function
dyg <- dygraphs::dySeries(dyg, y1_names[i], axis = "y", strokeWidth = 1.5, stepPlot = y1_step, plotter =multibar_combi_plotter(length(y2_names)))
}
}
# put stuff on y2 axis
dyg <- dygraphs::dyAxis(dyg, "y2", label = y2_label, independentTicks = T)
for(i in seq_along(y2_names)) {
dyg <- dygraphs::dySeries(dyg, y2_names[i], axis = "y2", strokeWidth = 1.5, stepPlot = y2_step)
}
return(dyg)
}
#we need to take into account all values and then leave out the ones we do not like
multibar_combi_plotter<-function(num_values){
#plotter function
plotter_text<-"function multiColumnBarPlotter(e) {
// We need to handle all the series simultaneously.
if (e.seriesIndex !== 0) return;
var g = e.dygraph;
var ctx = e.drawingContext;
var sets = e.allSeriesPoints;
var y_bottom = e.dygraph.toDomYCoord(0);
// Find the minimum separation between x-values.
// This determines the bar width.
var min_sep = Infinity;
for (var j = 0; j < sets.length-%s; j++) {
var points = sets[j];
for (var i = 1; i < points.length; i++) {
var sep = points[i].canvasx - points[i - 1].canvasx;
if (sep < min_sep) min_sep = sep;
}
}
var bar_width = Math.floor(2.0 / 3 * min_sep);
var fillColors = [];
var strokeColors = g.getColors();
for (var i = 0; i < strokeColors.length; i++) {
fillColors.push(strokeColors[i]);
}
for (var j = 0; j < sets.length-%s; j++) {
ctx.fillStyle = fillColors[j];
ctx.strokeStyle = strokeColors[j];
for (var i = 0; i < sets[j].length; i++) {
var p = sets[j][i];
var center_x = p.canvasx;
var x_left = center_x - (bar_width / 2) * (1 - j/(sets.length-%s-1));
ctx.fillRect(x_left, p.canvasy,
bar_width/sets.length, y_bottom - p.canvasy);
ctx.strokeRect(x_left, p.canvasy,
bar_width/sets.length, y_bottom - p.canvasy);
}
}
}"
custom_plotter <- sprintf(plotter_text, num_values, num_values, num_values)
return(custom_plotter)
}
reorder_xts<-function(xts_series,line_names){
bar_names<-colnames(xts_series)[!(colnames(xts_series)%in%line_names)]
xts_series<-xts_series[,c(bar_names,line_names)]
return(xts_series)
}
Some Explanation:
dy_position does all the plotting. It uses individual plotters per series axis.
reorder_xts is needed to make sure that all lines plots are at the right end of the xts. This is needed for the multibar plot. Because the java script is looping over all series (sets) to determine the width of the bars and we need to make sure we are not looping over the series which are line plots. Otherwise we have additional bars.
multibar_combi_plotter does exactly that. It takes a numeric parameter lines_names and modifies the javascript string so that it loops over all plots except for the line_names (i.e. last series in the right part of the xts). Notice several little %s in the string for the sprintfcommand! Afterwards it returns the plotter as character for the dySeries argument.
All the javascript code is taken directly from the examples in the dygraphs folder.
Here are some examples...
Examples:
dy_position(test,plot_title = "Test1", y2_names = c("Series_C","Series_D"),y1_label = "Axis1", y2_label = "Axis2", stacked=F)
dy_position(test,plot_title = "Test1", y2_names = c("Series_C","Series_D"),y1_label = "Axis1", y2_label = "Axis2", stacked=T)
dy_position(test,plot_title = "Test1", y2_names = c("Series_B","Series_C","Series_D"),y1_label = "Axis1", y2_label = "Axis2", stacked=T)
dy_position(test,plot_title = "Test1", y2_names = c("Series_D"),y1_label = "Axis1", y2_label = "Axis2", stacked=F)
dy_position(test,plot_title = "Test1", y2_names = c("Series_D"),y1_label = "Axis1", y2_label = "Axis2", stacked=T)
dy_position(test,plot_title = "Test1", y2_names = NULL ,y1_label = "Axis1", y2_label = "Axis2", stacked=F)
dy_position(test,plot_title = "Test1", y2_names = NULL ,y1_label = "Axis1", y2_label = "Axis2", stacked=T)
I am not sure this is exactly what you want. What I propose, comes close to the combination of a bar plot and a line plot, without the need to create a separate function.
You can set the type of plot per series, with dySeries. You can choose between lineplot (default), stepPlot, and stemPlot. In addition you may set to see the points with drawPoints and pointSize, you may also opt to fill the graph or not with fillGraph. For other options type ?dySeries
The code looks as follows:
library(dygraphs)
lungDeaths <- cbind(ldeaths, mdeaths)
dygraph(lungDeaths, main = "Main Title") %>%
dySeries("ldeaths", drawPoints = FALSE) %>%
dySeries("mdeaths", stepPlot = TRUE, fillGraph = TRUE)
Yielding this plot:
Please, let me know whether this is what you want.
After a bit of research I think that this would be simplest. At least that's the way it seems for me.
You would need to download the "barseries.js" file available at http://dygraphs.com/tests/plotters.html
Then the code would look like so
library(dygraphs)
dyBarSeries <- function(dygraph, name, ...) {
file <- "plotters/barseries.js" #you need to link to the downloaded file
plotter_ <- paste0(readLines(file, skipNul = T), collapse = "\n")
dots <- list(...)
do.call('dySeries', c(list(dygraph = dygraph, name = name, plotter =
plotter_), dots))
}
lungDeaths <- cbind(ldeaths, mdeaths)
dygraph(lungDeaths) %>%
dyBarSeries("ldeaths") %>%
dySeries("mdeaths")
Yielding this result
enter image description here