├── codecov.yml ├── LICENSE ├── .gitignore ├── data ├── eco2mix.rda ├── eco2mixBalance.rda └── d3.schemeCategory10.rda ├── tests ├── testthat.R └── testthat │ ├── test-flows.R │ ├── test-preprocess_args.R │ ├── test-minicharts.R │ └── test-prepare_js_args.R ├── vignettes ├── barcharts.png ├── piecharts.png ├── bubblecharts.png └── introduction.Rmd ├── shiny-demo └── leaflet-minicharts-demo │ ├── regions.rda │ ├── DESCRIPTION │ ├── global.R │ ├── ui.R │ └── server.R ├── inst ├── font-awesome-4.7.0 │ ├── fonts │ │ └── fontawesome-webfont.ttf │ └── css │ │ └── font-awesome.min.css └── minicharts.css ├── .Rbuildignore ├── javascript ├── index.js ├── package.json ├── sync_with.js ├── Gruntfile.js ├── flow_bindings.js ├── minichart_bindings.js ├── timeslider.js └── utils.js ├── R ├── zzz.R ├── minicharts_deps.R ├── preprocess_args.R ├── data-doc.R ├── popup_args.R ├── sync_with.R ├── add_flows.R ├── prepare_js_args.R └── add_minicharts.R ├── NAMESPACE ├── .travis.yml ├── leaflet.minicharts.Rproj ├── man ├── d3.schemeCategory10.Rd ├── eco2mix.Rd ├── popupArgs.Rd ├── syncWith.Rd ├── addFlows.Rd └── addMinicharts.Rd ├── appveyor.yml ├── DESCRIPTION ├── cran-comments.md ├── README.md └── NEWS /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | COPYRIGHT HOLDER: RTE Réseau de transport d’électricité 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | inst/doc 6 | javascript/node_modules 7 | -------------------------------------------------------------------------------- /data/eco2mix.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/data/eco2mix.rda -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(leaflet.minicharts) 3 | 4 | test_check("leaflet.minicharts") 5 | -------------------------------------------------------------------------------- /data/eco2mixBalance.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/data/eco2mixBalance.rda -------------------------------------------------------------------------------- /vignettes/barcharts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/vignettes/barcharts.png -------------------------------------------------------------------------------- /vignettes/piecharts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/vignettes/piecharts.png -------------------------------------------------------------------------------- /vignettes/bubblecharts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/vignettes/bubblecharts.png -------------------------------------------------------------------------------- /data/d3.schemeCategory10.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/data/d3.schemeCategory10.rda -------------------------------------------------------------------------------- /shiny-demo/leaflet-minicharts-demo/regions.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/shiny-demo/leaflet-minicharts-demo/regions.rda -------------------------------------------------------------------------------- /inst/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rte-antares-rpackage/leaflet.minicharts/HEAD/inst/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.travis\.yml$ 4 | ^appveyor\.yml$ 5 | shiny-demo 6 | javascript 7 | ^codecov\.yml$ 8 | ^cran-comments\.md$ 9 | ^CRAN-RELEASE$ 10 | -------------------------------------------------------------------------------- /shiny-demo/leaflet-minicharts-demo/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Title: Demo of leaflet.minicharts 2 | Author: Francois Guillem 3 | AuthorUrl: 4 | License: GPL-2 5 | DisplayMode: Showcase 6 | Tags: getting-started 7 | Type: Shiny 8 | -------------------------------------------------------------------------------- /javascript/index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | require("leaflet.flow"); 3 | require("leaflet.minichart"); 4 | require("./timeslider"); 5 | require("./minichart_bindings"); 6 | require("./flow_bindings"); 7 | require("./sync_with"); 8 | })(); 9 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | # Copyright © 2016 RTE Réseau de transport d’électricité 2 | #' @importFrom leaflet expandLimits invokeMethod %>% addLegend JS 3 | NULL 4 | 5 | globalVariables(c('d3.schemeCategory10', ".")) 6 | 7 | .onLoad <- function(libname, pkgname) { 8 | utils::data("d3.schemeCategory10", package=pkgname, envir=parent.env(environment())) 9 | } 10 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(addFlows) 4 | export(addMinicharts) 5 | export(clearFlows) 6 | export(clearMinicharts) 7 | export(popupArgs) 8 | export(removeFlows) 9 | export(removeMinicharts) 10 | export(syncWith) 11 | export(updateFlows) 12 | export(updateMinicharts) 13 | importFrom(leaflet,"%>%") 14 | importFrom(leaflet,JS) 15 | importFrom(leaflet,addLegend) 16 | importFrom(leaflet,expandLimits) 17 | importFrom(leaflet,invokeMethod) 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | sudo: required 5 | cache: packages 6 | 7 | r_github_packages: 8 | - rstudio/leaflet 9 | 10 | before_install: 11 | - sudo add-apt-repository ppa:ubuntugis/ubuntugis-unstable --yes 12 | - sudo apt-get --yes --force-yes update -qq 13 | - sudo apt-get install --yes libudunits2-dev libproj-dev libgeos-dev libgdal-dev 14 | 15 | r_packages: 16 | - rgeos 17 | 18 | after_success: 19 | - Rscript -e 'covr::codecov()' 20 | -------------------------------------------------------------------------------- /leaflet.minicharts.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageCheckArgs: --as-cran 23 | PackageRoxygenize: rd,collate,namespace 24 | -------------------------------------------------------------------------------- /R/minicharts_deps.R: -------------------------------------------------------------------------------- 1 | minichartDeps <- function() { 2 | minichartDep <- htmltools::htmlDependency( 3 | "minichart", 4 | "0.2.2", 5 | src = system.file(package = "leaflet.minicharts"), 6 | stylesheet = c("minicharts.css"), 7 | script = c("leaflet.minicharts.min.js") 8 | ) 9 | 10 | fontAwesomeDep <- htmltools::htmlDependency( 11 | "font-awesome", 12 | "4.7.0", 13 | src = system.file("font-awesome-4.7.0", package = "leaflet.minicharts"), 14 | stylesheet = "css/font-awesome.min.css" 15 | ) 16 | 17 | list(minichartDep, fontAwesomeDep) 18 | } 19 | -------------------------------------------------------------------------------- /man/d3.schemeCategory10.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data-doc.R 3 | \docType{data} 4 | \name{d3.schemeCategory10} 5 | \alias{d3.schemeCategory10} 6 | \title{d3 color palette} 7 | \format{ 8 | An object of class \code{character} of length 10. 9 | } 10 | \usage{ 11 | d3.schemeCategory10 12 | } 13 | \description{ 14 | A character vector containing ten colors. These colors are used as the default 15 | color palette 16 | } 17 | \references{ 18 | \url{https://github.com/d3/d3-scale} 19 | } 20 | \author{ 21 | Francois Guillem 22 | } 23 | \keyword{datasets} 24 | -------------------------------------------------------------------------------- /shiny-demo/leaflet-minicharts-demo/global.R: -------------------------------------------------------------------------------- 1 | library(dplyr) 2 | library(shiny) 3 | library(leaflet) 4 | library(leaflet.minicharts) 5 | 6 | data("eco2mix") 7 | load("regions.rda") 8 | 9 | # Remove data for the whole country 10 | prodRegions <- eco2mix %>% filter(area != "France") 11 | 12 | # Production columns 13 | prodCols <- names(prodRegions)[6:13] 14 | 15 | # Create base map 16 | tilesURL <- "http://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}" 17 | 18 | basemap <- leaflet(width = "100%", height = "400px") %>% 19 | addTiles(tilesURL) %>% 20 | addPolylines(data = regions, weight = 1, color = "brown") 21 | -------------------------------------------------------------------------------- /javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet.minicharts", 3 | "version": "0.2.1", 4 | "description": "javascript library for the R package leaflet.minicharts", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Francois Guillem", 10 | "license": "GPL-2.0+", 11 | "dependencies": { 12 | "leaflet.flow": "0.2.5", 13 | "leaflet.minichart": "0.2.5" 14 | }, 15 | "devDependencies": { 16 | "grunt": "^1.5.3", 17 | "grunt-browserify": "^6.0.0", 18 | "grunt-contrib-clean": "^2.0.0", 19 | "grunt-contrib-concat": "^1.0.1", 20 | "grunt-contrib-uglify": "^5.0.1", 21 | "grunt-contrib-watch": "^1.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shiny-demo/leaflet-minicharts-demo/ui.R: -------------------------------------------------------------------------------- 1 | # User interface 2 | library(shiny) 3 | library(leaflet) 4 | 5 | ui <- fluidPage( 6 | titlePanel("Demo of leaflet.minicharts"), 7 | p("This application uses the data.frame 'eco2mix', included in the 'leaflet.minicharts' packages.", 8 | "It contains the monthly electric production of french regions from 2013 to 2017."), 9 | 10 | sidebarLayout( 11 | 12 | sidebarPanel( 13 | selectInput("prods", "Select productions", choices = prodCols, multiple = TRUE), 14 | selectInput("type", "Chart type", choices = c("bar","pie", "polar-area", "polar-radius")), 15 | checkboxInput("labels", "Show values") 16 | ), 17 | 18 | mainPanel( 19 | leafletOutput("map") 20 | ) 21 | 22 | ) 23 | ) 24 | -------------------------------------------------------------------------------- /shiny-demo/leaflet-minicharts-demo/server.R: -------------------------------------------------------------------------------- 1 | # server function 2 | function(input, output, session) { 3 | # Initialize map 4 | output$map <- renderLeaflet({ 5 | basemap %>% 6 | addMinicharts( 7 | prodRegions$lng, prodRegions$lat, 8 | layerId = prodRegions$area, 9 | width = 45, height = 45 10 | ) 11 | }) 12 | 13 | # Update charts each time input value changes 14 | observe({ 15 | if (length(input$prods) == 0) { 16 | data <- 1 17 | } else { 18 | data <- prodRegions[, input$prods] 19 | } 20 | maxValue <- max(as.matrix(data)) 21 | 22 | leafletProxy("map", session) %>% 23 | updateMinicharts( 24 | prodRegions$area, 25 | chartdata = data, 26 | maxValues = maxValue, 27 | time = prodRegions$month, 28 | type = ifelse(length(input$prods) < 2, "polar-area", input$type), 29 | showLabels = input$labels 30 | ) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /tests/testthat/test-flows.R: -------------------------------------------------------------------------------- 1 | # context("flows") 2 | # 3 | # # Helper function 4 | # expect_invoke_js_method <- function(method, postProcess = I) { 5 | # map <- leaflet::leaflet() %>% 6 | # addFlows(0, 0, 1, 1, layerId = "a") %>% 7 | # postProcess() 8 | # 9 | # expect_true(all(method %in% map$jsmethods)) 10 | # } 11 | # 12 | # with_mock( 13 | # `leaflet::invokeMethod` = function(map, data, method, ...) { 14 | # map$jsargs = list(...) 15 | # map$jsmethods <- c(map$jsmethods, method) 16 | # map 17 | # }, 18 | # { 19 | # 20 | # test_that("One can add, update, clear and remove flows", { 21 | # expect_invoke_js_method("addFlows") 22 | # expect_invoke_js_method("updateFlows", function(map) { 23 | # updateFlows(map, layerId = "a", flow = 0.5) 24 | # }) 25 | # expect_invoke_js_method("clearFlows", clearFlows) 26 | # expect_invoke_js_method("removeFlows", function(map) { 27 | # removeFlows(map, "a") 28 | # }) 29 | # }) 30 | # 31 | # } 32 | # ) 33 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # DO NOT CHANGE the "init" and "install" sections below 2 | 3 | # Download script file from GitHub 4 | init: 5 | ps: | 6 | $ErrorActionPreference = "Stop" 7 | Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1" 8 | Import-Module '..\appveyor-tool.ps1' 9 | 10 | install: 11 | ps: Bootstrap 12 | 13 | # Adapt as necessary starting from here 14 | 15 | build_script: 16 | - travis-tool.sh install_deps 17 | 18 | test_script: 19 | - travis-tool.sh run_tests 20 | 21 | on_failure: 22 | - 7z a failure.zip *.Rcheck\* 23 | - appveyor PushArtifact failure.zip 24 | 25 | artifacts: 26 | - path: '*.Rcheck\**\*.log' 27 | name: Logs 28 | 29 | - path: '*.Rcheck\**\*.out' 30 | name: Logs 31 | 32 | - path: '*.Rcheck\**\*.fail' 33 | name: Logs 34 | 35 | - path: '*.Rcheck\**\*.Rout' 36 | name: Logs 37 | 38 | - path: '\*_*.tar.gz' 39 | name: Bits 40 | 41 | - path: '\*_*.zip' 42 | name: Bits 43 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: leaflet.minicharts 2 | Type: Package 3 | Title: Mini Charts for Interactive Maps 4 | Version: 0.6.3 5 | Authors@R: c( 6 | person(given = "Tatiana", 7 | family = "Vargas", 8 | role = c("aut", "cre"), 9 | email = "tatiana.vargas@rte-france.com"), 10 | person("Jalal-Edine", "ZAWAM", role = "aut"), 11 | person("Benoit", "Thieurmel", email = "benoit.thieurmel@datastorm.fr", role = "aut"), 12 | person("Francois", "Guillem", role = "aut"), 13 | person("RTE", role = "cph") 14 | ) 15 | Description: Add and modify small charts on an interactive map created with 16 | package 'leaflet'. These charts can be used to represent at same time multiple 17 | variables on a single map. 18 | License: GPL (>= 2) | file LICENSE 19 | Depends: R (>= 2.10) 20 | Imports: leaflet (>= 1.1.0), 21 | htmltools 22 | Encoding: UTF-8 23 | LazyData: true 24 | RoxygenNote: 7.2.2 25 | Suggests: knitr, 26 | rmarkdown, 27 | dplyr, 28 | shiny, 29 | manipulateWidget, 30 | testthat, 31 | covr 32 | VignetteBuilder: knitr 33 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | * local OS X install, R 3.6.1 3 | * ubuntu 14.04 (on travis-ci), R 3.6.1 4 | * win-builder (devel and release) 5 | 6 | ## R CMD check results 7 | 8 | 0 errors | 0 warnings | 1 note 9 | 10 | * Re-Fixing CRAN URL not in canonical form in README, Sorry for that. Thank you! 11 | 12 | ## Version: 0.6.3 13 | * update person, new maintener Tatiana Vargas 14 | * Fix/comment tests => 15 | - '`with_mock()` was deprecated in testthat 3.3.0. 16 | Please use `with_mocked_bindings()` instead' 17 | 18 | ## Test environments 19 | * R version 4.1.2 (2021-11-01) 20 | * Platform: x86_64-pc-linux-gnu (64-bit) 21 | * Running under: Ubuntu 22.04.4 LTS 22 | 23 | ## R CMD check results (only local warnings/notes) 24 | ❯ checking data for ASCII and uncompressed saves ... OK 25 | WARNING 26 | ‘qpdf’ is needed for checks on size reduction of PDFs 27 | 28 | ❯ checking for future file timestamps ... NOTE 29 | unable to verify current time 30 | 31 | 0 errors ✔ | 1 warning ✖ | 1 note ✖ 32 | 33 | 34 | # Re-Fixing OLD Travis URL in README (deleted), 35 | Sorry for that (didn't see that with local check `rcmdcheck::rcmdcheck(args = "--as-cran")`. 36 | Thank you! 37 | 38 | 39 | -------------------------------------------------------------------------------- /R/preprocess_args.R: -------------------------------------------------------------------------------- 1 | # Copyright © 2016 RTE Réseau de transport d’électricité 2 | 3 | #' Private function that prepare arguments for other functions. It transforms 4 | #' them in a table with one column per argument. In order to improve memory 5 | #' management, optional arguments with a single value are not added to the table 6 | #' but there value is stored in a specific list. 7 | #' 8 | #' @param required 9 | #' Named list of required parameters 10 | #' @param optional 11 | #' Named list of optional parameters 12 | #' 13 | #' @return 14 | #' A list with two elements: 15 | #' - options: data.frame with required args and variing optional args 16 | #' - staticOptions: a list with single value args. 17 | #' 18 | #' @noRd 19 | #' 20 | .preprocessArgs <- function(required, optional) { 21 | options <- do.call(data.frame, required) 22 | staticOptions <- list() 23 | for (o in names(optional)) { 24 | if (!is.null(optional[[o]])) { 25 | if (length(optional[[o]]) == 1) { 26 | staticOptions[[o]] <- optional[[o]] 27 | } else { 28 | options[[o]] <- optional[[o]] 29 | } 30 | } 31 | } 32 | list( 33 | options = options, 34 | staticOptions = staticOptions 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /man/eco2mix.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data-doc.R 3 | \docType{data} 4 | \name{eco2mix} 5 | \alias{eco2mix} 6 | \alias{eco2mixBalance} 7 | \title{Electric production, consumption and exchanges of France} 8 | \format{ 9 | An object of class \code{data.frame} with 686 rows and 22 columns. 10 | 11 | An object of class \code{data.frame} with 430 rows and 7 columns. 12 | } 13 | \usage{ 14 | eco2mix 15 | 16 | eco2mixBalance 17 | } 18 | \description{ 19 | \code{eco2mix} contains the electric production, consumption and exchanges 20 | of France from january 2010 to february 2017 and of 12 french regions from 21 | january 2013 to february 2017. 22 | 23 | In addition to the total production, the table contains one column for each 24 | type of production. The table also contains the latitude and longitude of 25 | the center of the regions. 26 | 27 | \code{eco2mixBalance} is an extract of \code{eco2mix} that contains only 28 | exchanges between France and neighbouring countries, in a convenient format 29 | to represent flows on a map. 30 | } 31 | \references{ 32 | "https://www.rte-france.com/eco2mix" 33 | } 34 | \author{ 35 | Francois Guillem 36 | } 37 | \keyword{datasets} 38 | -------------------------------------------------------------------------------- /R/data-doc.R: -------------------------------------------------------------------------------- 1 | #' Electric production, consumption and exchanges of France 2 | #' 3 | #' @description 4 | #' \code{eco2mix} contains the electric production, consumption and exchanges 5 | #' of France from january 2010 to february 2017 and of 12 french regions from 6 | #' january 2013 to february 2017. 7 | #' 8 | #' In addition to the total production, the table contains one column for each 9 | #' type of production. The table also contains the latitude and longitude of 10 | #' the center of the regions. 11 | #' 12 | #' \code{eco2mixBalance} is an extract of \code{eco2mix} that contains only 13 | #' exchanges between France and neighbouring countries, in a convenient format 14 | #' to represent flows on a map. 15 | #' 16 | #' @docType data 17 | #' @author Francois Guillem 18 | #' @references "https://www.rte-france.com/eco2mix" 19 | #' @keywords datasets 20 | "eco2mix" 21 | 22 | #' @rdname eco2mix 23 | "eco2mixBalance" 24 | 25 | 26 | #' d3 color palette 27 | #' 28 | #' @description 29 | #' A character vector containing ten colors. These colors are used as the default 30 | #' color palette 31 | #' @docType data 32 | #' @author Francois Guillem 33 | #' @references \url{https://github.com/d3/d3-scale} 34 | #' @keywords datasets 35 | "d3.schemeCategory10" 36 | -------------------------------------------------------------------------------- /javascript/sync_with.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | LeafletWidget.methods.syncWith = function(groupname) { 5 | var self = this; 6 | if (!LeafletWidget.syncGroups) { 7 | LeafletWidget.syncGroups = {}; 8 | LeafletWidget.syncGroups.sync = function(map, groupname) { 9 | var zoom = map.getZoom(); 10 | var center = map.getCenter(); 11 | if(LeafletWidget.syncGroups[groupname].length > 1){ 12 | for (var i = 0; i < LeafletWidget.syncGroups[groupname].length; i++) { 13 | LeafletWidget.syncGroups[groupname][i].off("move"); 14 | LeafletWidget.syncGroups[groupname][i].setView(center, zoom, {animate: false}); 15 | LeafletWidget.syncGroups[groupname][i].on("move", function() { 16 | LeafletWidget.syncGroups.sync(this, groupname); 17 | }); 18 | } 19 | } 20 | } 21 | } 22 | if (!LeafletWidget.syncGroups[groupname]) LeafletWidget.syncGroups[groupname] = []; 23 | 24 | LeafletWidget.syncGroups[groupname].push(self); 25 | 26 | // Bug with leafet > 1, & no really need...? 27 | self.on("move", function() { 28 | LeafletWidget.syncGroups.sync(self, groupname); 29 | }); 30 | 31 | self.syncGroup = groupname; 32 | LeafletWidget.syncGroups.sync(self, groupname); 33 | } 34 | }()); 35 | -------------------------------------------------------------------------------- /javascript/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 RTE Réseau de transport d’électricité 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | pkg: grunt.file.readJSON('package.json'), 8 | browserify: { 9 | options: { 10 | browserifyOptions : { 11 | debug: true 12 | } 13 | }, 14 | build: { 15 | files: { 16 | "../inst/leaflet.minicharts.min.js": ["index.js"] 17 | } 18 | } 19 | }, 20 | uglify: { 21 | options: { 22 | banner: '/*! <%= pkg.name %> <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %>\n' + 23 | 'Copyright © 2016 RTE Réseau de transport d’électricité */\n' 24 | }, 25 | build: { 26 | src: '../inst/leaflet.minicharts.min.js', 27 | dest: '../inst/leaflet.minicharts.min.js' 28 | }, 29 | }, 30 | watch: { 31 | build: { 32 | files: ["*.js"], 33 | tasks: ['browserify'] 34 | } 35 | } 36 | }); 37 | 38 | // Load plugins 39 | grunt.loadNpmTasks('grunt-browserify'); 40 | grunt.loadNpmTasks('grunt-contrib-uglify'); 41 | grunt.loadNpmTasks('grunt-contrib-watch'); 42 | // Default task(s). 43 | grunt.registerTask('build', ['browserify', 'uglify']); 44 | grunt.registerTask('default', ["build"]); 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /man/popupArgs.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/popup_args.R 3 | \name{popupArgs} 4 | \alias{popupArgs} 5 | \title{Options for popup generation} 6 | \usage{ 7 | popupArgs( 8 | showTitle = TRUE, 9 | showValues = TRUE, 10 | labels = NULL, 11 | supValues = NULL, 12 | supLabels = colnames(supValues), 13 | html = NULL, 14 | noPopup = FALSE, 15 | digits = NULL 16 | ) 17 | } 18 | \arguments{ 19 | \item{showTitle}{If \code{TRUE} layer id is displayed as title of 20 | popups.} 21 | 22 | \item{showValues}{If \code{TRUE}, values are displayed in popups} 23 | 24 | \item{labels}{Names of values. If \code{NULL}, column names of the data bound 25 | to a chart are used.} 26 | 27 | \item{supValues}{A \code{data.frame} containing additional values to display 28 | in popups.} 29 | 30 | \item{supLabels}{Names of the additional values.} 31 | 32 | \item{html}{Character vector containing custom html code for popups. You can 33 | use this parameter when you are not happy with the default popups.} 34 | 35 | \item{noPopup}{If \code{TRUE}, popups are not created.} 36 | 37 | \item{digits}{Max number of decimal digits to display for numeric values. If 38 | \code{NULL}, all digits are displayed.} 39 | } 40 | \value{ 41 | List containing options for popup generation 42 | } 43 | \description{ 44 | This function simply returns a list of options to control the generation of 45 | popups. 46 | } 47 | -------------------------------------------------------------------------------- /R/popup_args.R: -------------------------------------------------------------------------------- 1 | #' Options for popup generation 2 | #' 3 | #' This function simply returns a list of options to control the generation of 4 | #' popups. 5 | #' 6 | #' @param showTitle If \code{TRUE} layer id is displayed as title of 7 | #' popups. 8 | #' @param showValues If \code{TRUE}, values are displayed in popups 9 | #' @param labels Names of values. If \code{NULL}, column names of the data bound 10 | #' to a chart are used. 11 | #' @param supValues A \code{data.frame} containing additional values to display 12 | #' in popups. 13 | #' @param supLabels Names of the additional values. 14 | #' @param html Character vector containing custom html code for popups. You can 15 | #' use this parameter when you are not happy with the default popups. 16 | #' @param noPopup If \code{TRUE}, popups are not created. 17 | #' @param digits Max number of decimal digits to display for numeric values. If 18 | #' \code{NULL}, all digits are displayed. 19 | #' 20 | #' @return List containing options for popup generation 21 | #' 22 | #' @export 23 | popupArgs <- function(showTitle = TRUE, showValues = TRUE, labels = NULL, 24 | supValues = NULL, supLabels = colnames(supValues), 25 | html = NULL, noPopup = FALSE, digits = NULL) { 26 | if(!showTitle && !showValues & is.null(html) & is.null(supValues)) noPopup <- TRUE 27 | if (!is.null(html) | noPopup) supValues <- NULL 28 | as.list(environment()) 29 | } 30 | -------------------------------------------------------------------------------- /man/syncWith.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sync_with.R 3 | \name{syncWith} 4 | \alias{syncWith} 5 | \title{Synchronize multiple maps} 6 | \usage{ 7 | syncWith(map, groupname) 8 | } 9 | \arguments{ 10 | \item{map}{A leaflet map object created with \code{\link[leaflet]{leaflet}}.} 11 | 12 | \item{groupname}{Character string. All maps that use the same group name will 13 | be synchronized.} 14 | } 15 | \value{ 16 | The modified leaflet map object. 17 | } 18 | \description{ 19 | This function can be used when multiple leaflet maps are displayed on the 20 | same view (for instance in a shiny application or a Rmarkdown document) and 21 | one wants to synchronize their center, zoom and time. 22 | 23 | \code{syncWith()} can also be used with basic leaflet maps to synchronize 24 | only their zoom and center. 25 | } 26 | \examples{ 27 | if (require(manipulateWidget) & require(leaflet)) { 28 | 29 | # Synchronize zoom and center of basic maps. 30 | basicMap1 <- leaflet() \%>\% addTiles() \%>\% syncWith("basicmaps") 31 | basicMap2 <- leaflet() \%>\% addTiles() \%>\% syncWith("basicmaps") 32 | combineWidgets(basicMap1, basicMap2) 33 | 34 | # Synchronize time step of two maps that represent the evolution of some 35 | # variable. 36 | map1 <- leaflet() \%>\% addTiles() \%>\% 37 | addMinicharts(0, 40, chartdata = 1:10, time = 1:10) \%>\% 38 | syncWith("maps") 39 | map2 <- leaflet() \%>\% addTiles() \%>\% 40 | addMinicharts(0, 40, chartdata = 10:1, time = 1:10) \%>\% 41 | syncWith("maps") 42 | combineWidgets(map1, map2) 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /R/sync_with.R: -------------------------------------------------------------------------------- 1 | #' Synchronize multiple maps 2 | #' 3 | #' @description 4 | #' This function can be used when multiple leaflet maps are displayed on the 5 | #' same view (for instance in a shiny application or a Rmarkdown document) and 6 | #' one wants to synchronize their center, zoom and time. 7 | #' 8 | #' \code{syncWith()} can also be used with basic leaflet maps to synchronize 9 | #' only their zoom and center. 10 | #' 11 | #' @param groupname Character string. All maps that use the same group name will 12 | #' be synchronized. 13 | #' @inheritParams addMinicharts 14 | #' 15 | #' @return The modified leaflet map object. 16 | #' 17 | #' @examples 18 | #' if (require(manipulateWidget) & require(leaflet)) { 19 | #' 20 | #' # Synchronize zoom and center of basic maps. 21 | #' basicMap1 <- leaflet() %>% addTiles() %>% syncWith("basicmaps") 22 | #' basicMap2 <- leaflet() %>% addTiles() %>% syncWith("basicmaps") 23 | #' combineWidgets(basicMap1, basicMap2) 24 | #' 25 | #' # Synchronize time step of two maps that represent the evolution of some 26 | #' # variable. 27 | #' map1 <- leaflet() %>% addTiles() %>% 28 | #' addMinicharts(0, 40, chartdata = 1:10, time = 1:10) %>% 29 | #' syncWith("maps") 30 | #' map2 <- leaflet() %>% addTiles() %>% 31 | #' addMinicharts(0, 40, chartdata = 10:1, time = 1:10) %>% 32 | #' syncWith("maps") 33 | #' combineWidgets(map1, map2) 34 | #' 35 | #' } 36 | #' 37 | #' @export 38 | syncWith <- function(map, groupname) { 39 | # Add minichart dependency to the map dependencies 40 | map$dependencies <- c(map$dependencies, minichartDeps()) 41 | 42 | invokeMethod(map, data = leaflet::getMapData(map), "syncWith", groupname) 43 | } 44 | -------------------------------------------------------------------------------- /tests/testthat/test-preprocess_args.R: -------------------------------------------------------------------------------- 1 | context(".preprocessArgs") 2 | 3 | describe(".preprocessArgs", { 4 | it ("creates a data.frame with one column per argument", { 5 | opts <- .preprocessArgs(list(a = 1:3, b = 4:6), list(c = 7:9, d = 10:12)) 6 | expect_is(opts$options, "data.frame") 7 | expect_equal(dim(opts$options), c(3, 4)) 8 | expect_equal(names(opts$options), c("a", "b", "c", "d")) 9 | }) 10 | 11 | it ("separates static optional arguments", { 12 | opts <- .preprocessArgs(list(a = 1:3, b = 4:6), list(c = 7:9, d = 10)) 13 | expect_is(opts$options, "data.frame") 14 | expect_equal(dim(opts$options), c(3, 3)) 15 | expect_equal(names(opts$options), c("a", "b", "c")) 16 | expect_is(opts$staticOptions, "list") 17 | expect_equal(names(opts$staticOptions), "d") 18 | }) 19 | 20 | it ("does not separate static required arguments", { 21 | opts <- .preprocessArgs(list(a = 1:3, b = 4), list(c = 7:9, d = 10:12)) 22 | expect_is(opts$options, "data.frame") 23 | expect_equal(dim(opts$options), c(3, 4)) 24 | expect_equal(names(opts$options), c("a", "b", "c", "d")) 25 | expect_equal(length(opts$staticOptions), 0) 26 | }) 27 | 28 | it ("does not accept NULL required arguments", { 29 | expect_error(.preprocessArgs(list(a = 1:3, b = NULL), list(c = 7:9, d = 10:12))) 30 | }) 31 | 32 | it ("accepts NULL optional required", { 33 | opts <- .preprocessArgs(list(a = 1:3, b = 4:6), list(c = 7:9, d = NULL)) 34 | expect_is(opts$options, "data.frame") 35 | expect_equal(dim(opts$options), c(3, 3)) 36 | expect_equal(names(opts$options), c("a", "b", "c")) 37 | expect_equal(length(opts$staticOptions), 0) 38 | }) 39 | 40 | }) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leaflet.minicharts 2 | 3 | > Minicharts for dynamic [{leaflet}](https://rstudio.github.io/leaflet/) maps 4 | 5 | 6 | [![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/leaflet.minicharts)](https://cran.r-project.org/package=leaflet.minicharts) 7 | [![CRAN Downloads Badge](https://cranlogs.r-pkg.org/badges/leaflet.minicharts)](https://cran.r-project.org/package=leaflet.minicharts) 8 | 9 | 10 | 11 | For a few years now, it has become easy to create interactive maps with R thanks to the package `leaflet` by the Rstudio team. Nevertheless, it only provides only a few functions to create basic shapes on a map, so the information that can be represented on a single map is limited: if you have some data associated to some points, you can only represent at most two variables by drawing circles and changing their radius and color according to data. 12 | 13 | `leaflet.minicharts` is an R package that provides two functions to add and update small charts on an interactive maps created with the package `leaflet`. These charts can be used to represent as many variables as desired associated to geographical points. Currently, three types of chart are supported: barcharts (the default), pie charts and polar area charts (with two variants: "polar-area" and "polar-radius", where values are represented respectively by the area or the radius of the slices). 14 | 15 | Here are screenshots of sample outputs: 16 | 17 | ![](vignettes/piecharts.png) 18 | ![](vignettes/barcharts.png) 19 | ![](vignettes/bubblecharts.png) 20 | 21 | ## Installation and usage 22 | 23 | You can install the package from CRAN: 24 | 25 | ```r 26 | install.packages("leaflet.minicharts") 27 | ``` 28 | 29 | To see how simple it is to use this package, have a look at the [package vignette](https://cran.r-project.org/package=leaflet.minicharts/vignettes/introduction.html). 30 | 31 | 32 | ## Contributing 33 | 34 | Contributions to the library are welcome and can be submitted in the form of pull requests to this repository: 35 | 36 | https://github.com/rte-antares-rpackage/leaflet.minicharts 37 | 38 | This package contains some javascript code. To modify it requires `npm` and `grunt`. First modify files in the `javascript` folder. Then in a terminal, run the following commands: 39 | 40 | ``` 41 | cd javascript 42 | npm install 43 | grunt build 44 | ``` 45 | 46 | ## License Information 47 | 48 | Copyright 2015-2018 RTE (France) 49 | 50 | * RTE: http://www.rte-france.com 51 | 52 | This Source Code is subject to the terms of the GNU General Public License, version 2 or any higher version. If a copy of the GPL-v2 was not distributed with this file, You can obtain one at https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html. 53 | -------------------------------------------------------------------------------- /inst/minicharts.css: -------------------------------------------------------------------------------- 1 | /* popups */ 2 | 3 | .popup > h2 { 4 | color: #555; 5 | font-size: 16px; 6 | margin: 5px 0 0 0; 7 | } 8 | 9 | .popup table { 10 | border: none 1px; 11 | border-collapse: collapse; 12 | } 13 | 14 | .popup table td { 15 | border: none; 16 | padding: 2px 5px; 17 | border: solid 1px #DDDDDD; 18 | border-left-style: none; 19 | border-right-style: none; 20 | } 21 | 22 | .popup .key { 23 | font-weight:bold; 24 | color: #1f77b4; 25 | text-align: right; 26 | padding-right: 5px; 27 | } 28 | 29 | .popup .key::after { 30 | content:" :"; 31 | } 32 | 33 | .popup .value { 34 | text-align: right; 35 | } 36 | 37 | .popup hr { 38 | margin: 3px 0 6px 0; 39 | } 40 | 41 | /* time slider */ 42 | 43 | .leaflet-control-slider { 44 | font-size: 16px; 45 | height:16px; 46 | line-height:16px; 47 | position:relative; 48 | width: 120px; 49 | } 50 | 51 | p.time-slider-label { 52 | margin: 0; 53 | font-size: 16px; 54 | text-align: center; 55 | color: #333; 56 | } 57 | 58 | input[type=range].time-slider { 59 | -webkit-appearance: none; 60 | width: 100px; 61 | height:16px; 62 | margin: 0; 63 | padding: 0; 64 | margin-right: 5px; 65 | vertical-align: middle; 66 | position:absolute; 67 | } 68 | 69 | input[type=range].time-slider:focus { 70 | outline: none; 71 | } 72 | 73 | input[type=range].time-slider::-ms-track { 74 | width: 100%; 75 | cursor: pointer; 76 | background: transparent; 77 | border-color: transparent; 78 | color: transparent; 79 | margin:5px 0; 80 | } 81 | 82 | /* Special styling for WebKit/Blink */ 83 | input[type=range].time-slider::-webkit-slider-thumb { 84 | -webkit-appearance: none; 85 | border: 1px solid #777; 86 | height: 16px; 87 | width: 16px; 88 | border-radius: 8px; 89 | background: #ffffff; 90 | cursor: pointer; 91 | margin-top: -6px; 92 | box-sizing: border-box; 93 | } 94 | 95 | input[type=range].time-slider::-moz-range-thumb { 96 | border: 1px solid #777; 97 | height: 14px; 98 | width: 14px; 99 | border-radius: 7px; 100 | background: #ffffff; 101 | cursor: pointer; 102 | } 103 | 104 | input[type=range].time-slider::-ms-thumb { 105 | border: 1px solid #777777; 106 | height: 14px; 107 | width: 14px; 108 | border-radius: 7px; 109 | background: #ffffff; 110 | cursor: pointer; 111 | } 112 | 113 | input[type=range].time-slider::-webkit-slider-runnable-track { 114 | width: 100%; 115 | height: 6px; 116 | cursor: pointer; 117 | background: #ddd; 118 | border-radius: 3px; 119 | border: 1px solid #bbb; 120 | box-sizing: border-box; 121 | margin: 5px 0; 122 | } 123 | 124 | input[type=range].time-slider:focus::-webkit-slider-runnable-track { 125 | } 126 | 127 | input[type=range].time-slider::-moz-range-track { 128 | width: 100%; 129 | height: 6px; 130 | cursor: pointer; 131 | background: #ddd; 132 | border-radius: 3px; 133 | border: 1px solid #bbb; 134 | box-sizing: border-box; 135 | } 136 | 137 | input[type=range].time-slider::-ms-track { 138 | width: 100%; 139 | height: 6px; 140 | cursor: pointer; 141 | background: #ddd; 142 | border-radius: 3px; 143 | border: 1px solid #bbb; 144 | box-sizing: border-box; 145 | } 146 | 147 | .playpause { 148 | cursor: pointer; 149 | color: #999; 150 | position: absolute; 151 | right: 0; 152 | } 153 | -------------------------------------------------------------------------------- /javascript/flow_bindings.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var utils = require("./utils"); 5 | /* 6 | Add a segment on the map with a triangle in the middle representing its direction. 7 | 8 | @param data: 9 | data.frame with columns x0, y0, x1, y1 and optionnaly dir, color, opacity, weight 10 | popup and layerId 11 | 12 | */ 13 | LeafletWidget.methods.addFlows = function(options, timeLabels, initialTime, popupArgs, popupOptions) { 14 | var self = this; 15 | 16 | // Initialize time slider 17 | var timeId = utils.initTimeSlider(this, timeLabels, initialTime); 18 | 19 | // Add method to update time 20 | utils.addSetTimeIdMethod("Flow", "setStyle"); 21 | 22 | // Create flows 23 | utils.processOptions(options, function(opts, i, staticOpts) { 24 | for (var t = 0; t < opts.length; t++) { 25 | if (typeof opts[t].value != "undefined") opts[t].data = [opts[t].value]; 26 | else if (typeof staticOpts.value != "undefined") opts[t].data = [staticOpts.value]; 27 | 28 | if(popupArgs && popupArgs.supValues) { 29 | opts[t].popupData = popupArgs.supValues[i][t]; 30 | } 31 | if(popupArgs && popupArgs.html) { 32 | opts[t].popupHTML = popupArgs.html[i][t]; 33 | } 34 | } 35 | 36 | var l = L.flow( 37 | [staticOpts.lat0, staticOpts.lng0], 38 | [staticOpts.lat1, staticOpts.lng1], 39 | utils.getInitOptions(opts, staticOpts, timeId) 40 | ); 41 | l.opts = opts; 42 | l.timeId = timeId; 43 | if (staticOpts.layerId.indexOf("_flow") != 0) l.layerId = staticOpts.layerId; 44 | l.popupArgs = popupArgs; 45 | if (popupOptions) l.popupOptions = popupOptions; 46 | 47 | utils.setPopup(l, timeId); 48 | self.layerManager.addLayer(l, "flow", staticOpts.layerId); 49 | }); 50 | }; 51 | 52 | /* 53 | Update the style of directed segments 54 | 55 | @param data 56 | data.frame with columns layerId and optionnaly dir, color, opacity popup 57 | and weight 58 | 59 | */ 60 | LeafletWidget.methods.updateFlows = function(options, timeLabels, initialTime, popupArgs, popupOptions) { 61 | var self = this; 62 | 63 | var timeId = utils.initTimeSlider(this, timeLabels, initialTime); 64 | 65 | utils.processOptions(options, function(opts, i, staticOpts) { 66 | var l = self.layerManager.getLayer("flow", staticOpts.layerId); 67 | if (!l) return; 68 | 69 | if (popupArgs) l.popupArgs = popupArgs; 70 | if (popupOptions) l.popupOptions = popupOptions; 71 | for (var t = 0; t < opts.length; t++) { 72 | if (typeof opts[t].value != "undefined") opts[t].data = [opts[t].value]; 73 | else if (typeof staticOpts.value != "undefined") opts[t].data = [staticOpts.value]; 74 | else if (l.opts[t]) opts[t].data = l.opts[t].data 75 | 76 | if(popupArgs && popupArgs.supValues) { 77 | opts[t].popupData = popupArgs.supValues[i][t]; 78 | } else { 79 | if (l.opts[t]) opts[t].popupData = l.opts[t].popupData; 80 | } 81 | if(popupArgs && popupArgs.html) { 82 | opts[t].popupHTML = popupArgs.html[i][t]; 83 | } else { 84 | if (l.opts[t]) opts[t].popupHTML = l.opts[t].popupHTML; 85 | } 86 | } 87 | 88 | l.setStyle(utils.getInitOptions(opts, staticOpts, timeId)); 89 | l.opts = opts; 90 | l.timeId = timeId; 91 | 92 | utils.setPopup(l, timeId); 93 | }); 94 | }; 95 | 96 | utils.addRemoveMethods("Flow", "flow"); 97 | }()); 98 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | Changes in version 0.6.3 (2025-10-28) 2 | * update maintainer person 3 | 4 | 5 | 6 | Changes in version 0.6.2 (2021-04-07) 7 | 8 | NEW FEATURES: 9 | * add popupOptions 10 | 11 | Changes in version 0.6.1 (2019-12-16) 12 | 13 | BUGFIXES: 14 | * Fix / improve syncWith 15 | 16 | Changes in version 0.6.0 (2019-11-26) 17 | 18 | BUGFIXES: 19 | * Fix syncGroups 20 | * Fix flow with NULL value / nearest position 21 | 22 | Changes in version 0.5.4 (2018-06-06) 23 | 24 | BUGFIXES: 25 | * Fix syncGroups using leaflet 2.0.0 26 | * Fix clear and remove methods leaflet 2.0.0 27 | 28 | Changes in version 0.5.3 (2018-04-25) 29 | 30 | * leaflet.minicharts take into account leaflet 2.0.0 and leaflet.flow 0.2.3 31 | 32 | Changes in version 0.5.2 (2017-12-06) 33 | 34 | BUGFIXES: 35 | * update file description 36 | * Invalid maxValues (0, NA, ...) 37 | 38 | Changes in version 0.5.1 (2017-08-17) 39 | 40 | BUGFIXES: 41 | * updateMinicharts() was incapable of updating color palette (#19) 42 | 43 | Changes in version 0.5.0 (2017-07-10) 44 | 45 | NEW FEATURES: 46 | * addMinicharts() and updateMinicharts() gain a new parameter "onChange" that permits to execute some arbitrary javascript code each time a minichart is updated. 47 | * The package now uses unit testing, code coverage and continuous integration in order to improve code quality and reliability. 48 | 49 | BUGFIXES: 50 | * Some warnings that appeared with R 3.4 have been removed. 51 | * Parameter "initialTime" was not working correctly in some situations. 52 | * Parameter "fillColor" was ignored if someone tried to add a single minicharts. 53 | 54 | 55 | Changes in version 0.4.0 (2017-06-19) 56 | 57 | NEW FEATURES: 58 | * New function syncWith() to synchronize zoom, center and time of multiple leaflet maps. 59 | 60 | 61 | Changes in version 0.3.1 (2017-05-30) 62 | 63 | BUGFIXES: 64 | * addMinicharts() uses functions that are only available in package leaflet >= 1.1.0. The package now requires this version of leaflet. 65 | 66 | 67 | Changes in version 0.3 (2017-05-19) 68 | 69 | NEW FEATURES: 70 | * Default popups are now automatically generated for charts and flows. A new function "popupArgs()" has been added to control how popups are generated. 71 | * The size of the JSON data passed from R to javascript has been optimized. In some situations it has been divided by 10, so maps are generated quicker and when they are saved on harddrive, the resulting files are smaller. 72 | 73 | BUGFIXES: 74 | * The default color palette was not found when leaflet.minicharts was imported in another package. 75 | * add/updateMinicharts/Flows were crashing if layerId was a factor and some levels were missing. 76 | * Some bugs were occuring if after an update of a map, the number of timesteps increased. 77 | 78 | 79 | Changes in version 0.2 (2017-05-05) 80 | 81 | BREAKING CHANGES: 82 | * Argument "data" of addMinicharts() and updateMinicharts() has been renamed "chartdata" to avoid confusion with parameter "data" of function leaflet(). 83 | 84 | NEW FEATURES: 85 | * addMinicharts() now automatically adds a legend to the map by default. 86 | * addMinicharts() has new parameters "labelMinSize" and "labelMaxSize" to control label size. 87 | * addMinicharts() has a new parameter "time" that can be used to create an animated map that represents the evolution of one or more variables. Animations can be shared by saving the map as html or including it in a Ramrkdown document. No need to use Shiny anymore! 88 | * New functions removeMinicharts() and clearMinicharts() to remove some or all minicharts from a map. 89 | * It is now possible to represent flows between two points with function addFlow() and the associated functions updateFlows(), removeFlows() and clearFlows(). 90 | 91 | BUGFIXES: 92 | * addMinicharts was crashing if user tried to add a single chart to a map. 93 | -------------------------------------------------------------------------------- /man/addFlows.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/add_flows.R 3 | \name{addFlows} 4 | \alias{addFlows} 5 | \alias{updateFlows} 6 | \alias{removeFlows} 7 | \alias{clearFlows} 8 | \title{Add or modify flows on a leaflet map} 9 | \usage{ 10 | addFlows( 11 | map, 12 | lng0, 13 | lat0, 14 | lng1, 15 | lat1, 16 | color = "blue", 17 | flow = 1, 18 | opacity = 1, 19 | dir = NULL, 20 | time = NULL, 21 | popup = popupArgs(labels = "Flow"), 22 | layerId = NULL, 23 | timeFormat = NULL, 24 | initialTime = NULL, 25 | maxFlow = max(abs(flow)), 26 | minThickness = 1, 27 | maxThickness = 20, 28 | popupOptions = NULL 29 | ) 30 | 31 | updateFlows( 32 | map, 33 | layerId, 34 | color = NULL, 35 | flow = NULL, 36 | opacity = NULL, 37 | dir = NULL, 38 | time = NULL, 39 | popup = NULL, 40 | timeFormat = NULL, 41 | initialTime = NULL, 42 | maxFlow = NULL, 43 | minThickness = 1, 44 | maxThickness = 20, 45 | popupOptions = NULL 46 | ) 47 | 48 | removeFlows(map, layerId) 49 | 50 | clearFlows(map) 51 | } 52 | \arguments{ 53 | \item{map}{A leaflet map object created with \code{\link[leaflet]{leaflet}}.} 54 | 55 | \item{lng0}{Longitude of the origin of the flow.} 56 | 57 | \item{lat0}{Latitude of the origin of the flow.} 58 | 59 | \item{lng1}{Longitude of the destination of the flow.} 60 | 61 | \item{lat1}{Latitude of the destination of the flow.} 62 | 63 | \item{color}{Color of the flow.} 64 | 65 | \item{flow}{Value of the flow between the origin and the destination. If 66 | argument \code{dir} is not set, negative values are interpreted as flows 67 | from destination to origin.} 68 | 69 | \item{opacity}{Opacity of the flow.} 70 | 71 | \item{dir}{Direction of the flow. 1 indicates that the flow goes from origin 72 | to destination and -1 indicates that it goes from destination to origin. If 73 | 0, the arrow is not drawn. If \code{NULL}, then it is equal to the sign of 74 | \code{weight}.} 75 | 76 | \item{time}{A vector with length equal to the number of rows in \code{chartdata} 77 | and containing either numbers representing time indices or dates or 78 | datetimes. Each unique value must appear as many times as the others. This 79 | parameter can be used when one wants to represent the evolution of some 80 | variables on a map.} 81 | 82 | \item{popup}{Options that control popup generation.} 83 | 84 | \item{layerId}{An ID variable. It is mandatory when one wants to update some 85 | chart with \code{updateMinicharts}.} 86 | 87 | \item{timeFormat}{Character string used to format dates and times when 88 | argument \code{time} is a \code{Date}, \code{POSIXct} or \code{POSIXlt} 89 | object. See \code{\link[base]{strptime}} for more information.} 90 | 91 | \item{initialTime}{This argument can be used to set the initial time step 92 | shown when the map is created. It is used only when argument \code{time} is 93 | set.} 94 | 95 | \item{maxFlow}{Maximal value a flow could take.} 96 | 97 | \item{minThickness}{minimal thickness of the line that represents the flow.} 98 | 99 | \item{maxThickness}{maximal thickness of the line that represents the flow.} 100 | 101 | \item{popupOptions}{Change default popupOptions (ex : autoClose, maxHeight, closeButton ...) 102 | See \code{\link[leaflet]{popupOptions}} for more informations.} 103 | } 104 | \value{ 105 | The modified leaflet map object. 106 | } 107 | \description{ 108 | These functions can be used to represent flows and their evolution on a map 109 | created with \code{\link[leaflet]{leaflet}()}. Flows are simply represented 110 | by a segment between two points with an arrow at its center that indicates the 111 | direction of the flow. 112 | } 113 | \examples{ 114 | 115 | require(leaflet) 116 | 117 | # Toy example 118 | leaflet() \%>\% addTiles() \%>\% 119 | addFlows(0, 0, 1, 1, flow = 10) 120 | 121 | # Electric exchanges between France and neighboring countries 122 | data("eco2mixBalance") 123 | bal <- eco2mixBalance 124 | leaflet() \%>\% addTiles() \%>\% 125 | addFlows( 126 | bal$lng0, bal$lat0, bal$lng1, bal$lat1, 127 | flow = bal$balance, 128 | time = bal$month 129 | ) 130 | 131 | # popupOptions 132 | data("eco2mixBalance") 133 | bal <- eco2mixBalance 134 | leaflet() \%>\% addTiles() \%>\% 135 | addFlows( 136 | bal$lng0, bal$lat0, bal$lng1, bal$lat1, 137 | flow = bal$balance, 138 | time = bal$month, 139 | popupOptions = list(closeOnClick = FALSE, autoClose = FALSE) 140 | ) 141 | 142 | } 143 | -------------------------------------------------------------------------------- /javascript/minichart_bindings.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 RTE Réseau de transport d’électricité 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | var utils = require("./utils"); 7 | var d3 = require("d3"); 8 | 9 | LeafletWidget.methods.addMinicharts = function(options, data, maxValues, colorPalette, timeLabels, initialTime, popupArgs, onChange, popupOptions) { 10 | var self = this; 11 | var timeId = utils.initTimeSlider(this, timeLabels, initialTime); 12 | 13 | // Add method to update time 14 | utils.addSetTimeIdMethod("Minichart", "setOptions"); 15 | 16 | // Create and add minicharts to the map 17 | utils.processOptions(options, function(opts, i, staticOpts) { 18 | for (var t = 0; t < opts.length; t++) { 19 | if (data) { 20 | opts[t].data = data[i][t]; 21 | } 22 | 23 | if(popupArgs.supValues) { 24 | opts[t].popupData = popupArgs.supValues[i][t]; 25 | } 26 | 27 | if(popupArgs.html) { 28 | opts[t].popupHTML = popupArgs.html[i][t]; 29 | } 30 | 31 | if (maxValues) opts[t].maxValues = maxValues; 32 | 33 | if (!opts[t].data || opts[t].data.length == 1) { 34 | opts[t].colors = opts[t].fillColor || staticOpts.fillColor || d3.schemeCategory10[0]; 35 | } else { 36 | opts[t].colors = colorPalette || d3.schemeCategory10; 37 | } 38 | } 39 | 40 | var l = L.minichart( 41 | [staticOpts.lat, staticOpts.lng], 42 | utils.getInitOptions(opts, staticOpts, timeId) 43 | ); 44 | 45 | // Keep a reference of colors and data for later use. 46 | l.opts = opts; 47 | l.colorPalette = colorPalette || d3.schemeCategory10; 48 | l.timeId = timeId; 49 | l.popupArgs = popupArgs; 50 | if (popupOptions) l.popupOptions = popupOptions; 51 | if (staticOpts.layerId.indexOf("_minichart") != 0) l.layerId = staticOpts.layerId; 52 | 53 | // Popups 54 | var popup = utils.setPopup(l, timeId); 55 | 56 | self.layerManager.addLayer(l, "minichart", staticOpts.layerId); 57 | 58 | if (onChange) { 59 | l.onChange = onChange; 60 | l.onChange(utils.getInitOptions(opts, staticOpts, timeId), popup, d3); 61 | } 62 | }); 63 | }; 64 | 65 | LeafletWidget.methods.updateMinicharts = function(options, data, maxValues, colorPalette, timeLabels, initialTime, popupArgs, legendLab, onChange, popupOptions) { 66 | var self = this; 67 | var timeId = utils.initTimeSlider(this, timeLabels, initialTime); 68 | 69 | utils.processOptions(options, function(opts, i, staticOpts) { 70 | var l = self.layerManager.getLayer("minichart", staticOpts.layerId); 71 | // If the layer does not exist quit the function 72 | if (!l) return; 73 | 74 | if (popupArgs) l.popupArgs = popupArgs; 75 | else if(data && legendLab) {l.popupArgs.labels = legendLab} 76 | if (popupOptions) l.popupOptions = popupOptions; 77 | for (var t = 0; t < opts.length; t++) { // loop over time steps 78 | if (data) { 79 | opts[t].data = data[i][t]; 80 | } else { 81 | if (l.opts[t]) opts[t].data = l.opts[t].data; 82 | } 83 | 84 | if (popupArgs && popupArgs.supValues) { 85 | opts[t].popupData = popupArgs.supValues[i][t]; 86 | } else { 87 | if (l.opts[t]) opts[t].popupData = l.opts[t].popupData; 88 | } 89 | if (popupArgs && popupArgs.html) { 90 | opts[t].popupHTML = popupArgs.html[i][t]; 91 | } else { 92 | if (l.opts[t]) opts[t].popupHTML = l.opts[t].popupHTML; 93 | } 94 | 95 | if (opts[t].data.length == 1) { 96 | if (opts[t].fillColor) opts[t].colors = opts[t].fillColor; 97 | else if (staticOpts.fillColor) opts[t].colors = staticOpts.fillColor; 98 | else if (l.opts[t] && l.opts[t].fillColor) opts[t].colors = l.opts[t].fillColor; 99 | else opts[t].colors = l.opts[0].fillColor; 100 | } else if (colorPalette) { 101 | opts[t].colors = colorPalette; 102 | } else { 103 | opts[t].colors = l.colorPalette; 104 | } 105 | 106 | if (maxValues) opts[t].maxValues = maxValues; 107 | } 108 | 109 | l.opts = opts; 110 | l.setOptions(utils.getInitOptions(opts, staticOpts, timeId)); 111 | if (onChange) { 112 | l.onChange = eval(onChange); 113 | } 114 | 115 | var popup = utils.setPopup(l, timeId); 116 | if (l.onChange) l.onChange(utils.getInitOptions(opts, staticOpts, timeId), popup, d3); 117 | 118 | }); 119 | }; 120 | 121 | utils.addRemoveMethods("Minichart", "minichart"); 122 | }()); 123 | -------------------------------------------------------------------------------- /javascript/timeslider.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | L.TimeSlider = L.Control.extend({ 5 | options: { 6 | position: "bottomright", 7 | timeLabels: null, 8 | interval: 1000, 9 | onTimeIdChange: function(timeId) {console.log(timeId)} 10 | }, 11 | 12 | initialize: function(options) { 13 | L.Control.prototype.initialize.call(this, options); 14 | 15 | var container = L.DomUtil.create('div', "leaflet-bar leaflet-control"); 16 | container.style.padding = "5px"; 17 | container.style.backgroundColor = 'white'; 18 | 19 | var label = L.DomUtil.create("p", "time-slider-label", container); 20 | var sliderContainer = L.DomUtil.create("div", "leaflet-control-slider", container); 21 | 22 | var slider = L.DomUtil.create("input", "time-slider", sliderContainer); 23 | slider.type = "range"; 24 | slider.min = 0; 25 | slider.max = options.timeLabels.length - 1; 26 | slider.value = 0; 27 | 28 | var btn = L.DomUtil.create("i", "playpause fa fa-play", sliderContainer); 29 | 30 | this._container = container; 31 | this._slider = slider; 32 | this._play = false; 33 | this._btn = btn; 34 | this._label = label; 35 | }, 36 | 37 | onAdd: function(map) { 38 | var self = this; 39 | self.map = map; 40 | 41 | L.DomEvent.disableClickPropagation(self._container); 42 | self._slider.onchange = function(e) { 43 | self.setTimeId(self.getTimeId(), true); 44 | self.updateGroup(function(map) { 45 | map.controls._controlsById.tslider.setTimeId(self.getTimeId()); 46 | }); 47 | }; 48 | self._btn.onclick = function(e) { 49 | self.playPause(!self._play); 50 | self.updateGroup(function(map) { 51 | map.controls._controlsById.tslider.playPause(self._play); 52 | }); 53 | }; 54 | 55 | self.setTimeLabels(self.options.timeLabels); 56 | 57 | return self._container; 58 | }, 59 | 60 | playPause: function(play) { 61 | var self = this; 62 | self._play = play; 63 | if(self._play) { 64 | self._btn.className = "playpause fa fa-pause"; 65 | if (self.getTimeId() == self._slider.max) { 66 | self.setTimeId(0); 67 | } 68 | 69 | self._intervalId = setInterval(function() { 70 | self.setTimeId(self.getTimeId() + 1); 71 | if (self.getTimeId() == self._slider.max) { 72 | clearInterval(self._intervalId); 73 | self._play = false; 74 | self._btn.className = "playpause fa fa-play"; 75 | } 76 | }, self.options.interval); 77 | } else { 78 | clearInterval(self._intervalId); 79 | self._btn.className = "playpause fa fa-play"; 80 | } 81 | }, 82 | 83 | setTimeId: function(timeId, skip) { 84 | var self = this; 85 | if (!skip) self._slider.value = timeId; 86 | self._label.innerHTML = self.options.timeLabels[timeId]; 87 | self.options.onTimeIdChange(timeId); 88 | }, 89 | 90 | getTimeId: function() { 91 | return parseInt(this._slider.value); 92 | }, 93 | 94 | setTimeLabels: function(timeLabels) { 95 | if (timeLabels === null) return; 96 | var self = this; 97 | 98 | if (typeof timeLabels == "undefined") { 99 | timeLabels = ["undefined"]; 100 | } else if (timeLabels.constructor != Array) { 101 | timeLabels = [timeLabels]; 102 | } 103 | 104 | if (timeLabels.length < 2) { 105 | self._container.style.display = "none"; 106 | } else { 107 | self._container.style.display = "block"; 108 | } 109 | 110 | var currentTimeLabel = self.options.timeLabels[self.getTimeId()]; 111 | self.options.timeLabels = timeLabels; 112 | var newTimeId = self.toTimeId(currentTimeLabel); 113 | 114 | self._slider.max = timeLabels.length - 1; 115 | self.setTimeId(newTimeId); 116 | }, 117 | 118 | toTimeId: function(label) { 119 | if (typeof this.options.timeLabels == "undefined" || 120 | this.options.timeLabels.constructor != Array) { 121 | return 0; 122 | } 123 | 124 | var timeId = this.options.timeLabels.indexOf(label); 125 | if (timeId == -1) timeId = 0; 126 | return timeId; 127 | }, 128 | 129 | updateGroup: function(updateFun) { 130 | var self = this; 131 | if (self.map.syncGroup) { 132 | var syncMap; 133 | for (var i = 0; i < LeafletWidget.syncGroups[self.map.syncGroup].length; i++) { 134 | syncMap = LeafletWidget.syncGroups[self.map.syncGroup][i]; 135 | if (syncMap != self.map && syncMap.controls._controlsById.tslider) { 136 | updateFun(syncMap); 137 | } 138 | } 139 | } 140 | } 141 | }); 142 | 143 | L.timeSlider = function(options) { 144 | return new L.TimeSlider(options); 145 | }; 146 | }()); 147 | -------------------------------------------------------------------------------- /R/add_flows.R: -------------------------------------------------------------------------------- 1 | #' Add or modify flows on a leaflet map 2 | #' 3 | #' These functions can be used to represent flows and their evolution on a map 4 | #' created with \code{\link[leaflet]{leaflet}()}. Flows are simply represented 5 | #' by a segment between two points with an arrow at its center that indicates the 6 | #' direction of the flow. 7 | #' 8 | #' @param lng0 Longitude of the origin of the flow. 9 | #' @param lat0 Latitude of the origin of the flow. 10 | #' @param lng1 Longitude of the destination of the flow. 11 | #' @param lat1 Latitude of the destination of the flow. 12 | #' @param color Color of the flow. 13 | #' @param flow Value of the flow between the origin and the destination. If 14 | #' argument \code{dir} is not set, negative values are interpreted as flows 15 | #' from destination to origin. 16 | #' @param opacity Opacity of the flow. 17 | #' @param dir Direction of the flow. 1 indicates that the flow goes from origin 18 | #' to destination and -1 indicates that it goes from destination to origin. If 19 | #' 0, the arrow is not drawn. If \code{NULL}, then it is equal to the sign of 20 | #' \code{weight}. 21 | #' @param maxFlow Maximal value a flow could take. 22 | #' @param minThickness minimal thickness of the line that represents the flow. 23 | #' @param maxThickness maximal thickness of the line that represents the flow. 24 | #' @inheritParams addMinicharts 25 | #' 26 | #' @return 27 | #' The modified leaflet map object. 28 | #' @examples 29 | #' 30 | #' require(leaflet) 31 | #' 32 | #' # Toy example 33 | #' leaflet() %>% addTiles() %>% 34 | #' addFlows(0, 0, 1, 1, flow = 10) 35 | #' 36 | #' # Electric exchanges between France and neighboring countries 37 | #' data("eco2mixBalance") 38 | #' bal <- eco2mixBalance 39 | #' leaflet() %>% addTiles() %>% 40 | #' addFlows( 41 | #' bal$lng0, bal$lat0, bal$lng1, bal$lat1, 42 | #' flow = bal$balance, 43 | #' time = bal$month 44 | #' ) 45 | #' 46 | #' # popupOptions 47 | #' data("eco2mixBalance") 48 | #' bal <- eco2mixBalance 49 | #' leaflet() %>% addTiles() %>% 50 | #' addFlows( 51 | #' bal$lng0, bal$lat0, bal$lng1, bal$lat1, 52 | #' flow = bal$balance, 53 | #' time = bal$month, 54 | #' popupOptions = list(closeOnClick = FALSE, autoClose = FALSE) 55 | #' ) 56 | #' 57 | #' @export 58 | addFlows <- function(map, lng0, lat0, lng1, lat1, color = "blue", flow = 1, 59 | opacity = 1, dir = NULL, time = NULL, popup = popupArgs(labels = "Flow"), 60 | layerId = NULL, 61 | timeFormat = NULL, initialTime = NULL, maxFlow = max(abs(flow)), 62 | minThickness = 1, maxThickness = 20, popupOptions = NULL) { 63 | if (is.null(time)) time <- 1 64 | if (is.null(layerId)) layerId <- sprintf("_flow (%s,%s) -> (%s,%s)", lng0, lat0, lng1, lat1) 65 | 66 | options <- .preprocessArgs( 67 | required = list(lng0 = lng0, lat0 = lat0, lng1 = lng1, lat1 = lat1, layerId = layerId, time = time), 68 | optional = list(dir = dir, color = color, value = flow, maxValue = maxFlow, 69 | minThickness = minThickness, maxThickness = maxThickness, 70 | opacity = opacity) 71 | ) 72 | 73 | args <- .prepareJSArgs(options, NULL, popup, 74 | initialTime = initialTime, timeFormat = timeFormat) 75 | 76 | # Add minichart and font-awesome to the map dependencies 77 | map$dependencies <- c(map$dependencies, minichartDeps()) 78 | 79 | invokeMethod(map, data = leaflet::getMapData(map), "addFlows", args$options, 80 | args$timeLabels, args$initialTime, args$popupArgs, popupOptions) %>% 81 | expandLimits(c(lat0, lat1), c(lng0, lng1)) 82 | } 83 | 84 | #' @rdname addFlows 85 | #' @export 86 | updateFlows <- function(map, layerId, color = NULL, flow = NULL, opacity = NULL, 87 | dir = NULL, time = NULL, popup = NULL, 88 | timeFormat = NULL, initialTime = NULL, maxFlow = NULL, 89 | minThickness = 1, maxThickness = 20, popupOptions = NULL) { 90 | if (is.null(time)) time <- 1 91 | 92 | options <- .preprocessArgs( 93 | required = list(layerId = layerId, time = time), 94 | optional = list(dir = dir, color = color, value = flow, maxValue = maxFlow, 95 | minThickness = minThickness, maxThickness = maxThickness, 96 | opacity = opacity) 97 | ) 98 | 99 | args <- .prepareJSArgs(options, NULL, popup, 100 | initialTime = initialTime, timeFormat = timeFormat) 101 | 102 | if(is.null(flow)) { 103 | args$timeLabels <- NULL 104 | } 105 | 106 | invokeMethod(map, data = leaflet::getMapData(map), "updateFlows", args$options, 107 | args$timeLabels, args$initialTime, args$popupArgs, popupOptions) 108 | } 109 | 110 | #' @rdname addFlows 111 | #' @export 112 | removeFlows <- function(map, layerId) { 113 | invokeMethod(map, leaflet::getMapData(map), "removeFlows", layerId) 114 | } 115 | 116 | #' @rdname addFlows 117 | #' @export 118 | clearFlows <- function(map) { 119 | invokeMethod(map, leaflet::getMapData(map), "clearFlows") %>% 120 | leaflet::removeControl("minichartsLegend") 121 | } 122 | -------------------------------------------------------------------------------- /tests/testthat/test-minicharts.R: -------------------------------------------------------------------------------- 1 | # context("minichart") 2 | # 3 | # # Helper function 4 | # expect_invoke_js_method <- function(method, postProcess = I) { 5 | # map <- leaflet::leaflet() %>% 6 | # addMinicharts(0, 0, 1:3, layerId = "a") %>% 7 | # postProcess() 8 | # 9 | # expect_true(all(method %in% map$jsmethods)) 10 | # } 11 | # 12 | # with_mock( 13 | # `leaflet::invokeMethod` = function(map, data, method, ...) { 14 | # map$jsargs <- list(...) 15 | # map$jsmethods <- c(map$jsmethods, method) 16 | # map 17 | # }, 18 | # `leaflet::addLegend` = function(map, ...) { 19 | # map$legendArgs <- list(...) 20 | # map 21 | # }, 22 | # `leaflet::removeControl` = function(map, id) { 23 | # map$controlRemoved <- id 24 | # map 25 | # }, 26 | # { 27 | # 28 | # test_that("One can add, update, clear and remove minicharts", { 29 | # expect_invoke_js_method("addMinicharts") 30 | # expect_invoke_js_method("updateMinicharts", function(map) { 31 | # updateMinicharts(map, layerId = "a", chartdata = 1:4) 32 | # }) 33 | # expect_invoke_js_method("clearMinicharts", clearMinicharts) 34 | # expect_invoke_js_method("removeMinicharts", function(map) { 35 | # removeMinicharts(map, "a") 36 | # }) 37 | # }) 38 | # 39 | # describe("addMinicharts", { 40 | # it("correctly manages the 'labels' argument", { 41 | # # No labels 42 | # map <- leaflet::leaflet() %>% addMinicharts(0, 0, 1:3, layerId = "a") 43 | # expect_equal(map$jsargs[[1]][[1]]$static$labels, "none") 44 | # # Auto labels 45 | # map <- leaflet::leaflet() %>% 46 | # addMinicharts(0, 0, 1:3, layerId = "a", showLabels = TRUE) 47 | # expect_equal(map$jsargs[[1]][[1]]$static$labels, "auto") 48 | # # Custom labels 49 | # map <- leaflet::leaflet() %>% 50 | # addMinicharts(0, 0, 1, layerId = "a", showLabels = TRUE, 51 | # labelText = "test") 52 | # expect_equal(map$jsargs[[1]][[1]]$static$labels, "test") 53 | # }) 54 | # 55 | # it("adds a legend when chartdata has multiple named columns", { 56 | # map <- leaflet::leaflet() %>% 57 | # addMinicharts(0, 0, data.frame(a = 1, b = 2), layerId = "a") 58 | # expect_false(is.null(map$legendArgs)) 59 | # expect_equal(map$legendArgs$labels, I(c("a", "b"))) 60 | # # No names, no legend 61 | # map <- leaflet::leaflet() %>% addMinicharts(0, 0, 1:3, layerId = "a") 62 | # expect_null(map$legendArgs) 63 | # # One column, no legend 64 | # map <- leaflet::leaflet() %>% 65 | # addMinicharts(0, 0, data.frame(a = 1), layerId = "a") 66 | # expect_null(map$legendArgs) 67 | # # Argument legend is FALSE 68 | # map <- leaflet::leaflet() %>% 69 | # addMinicharts(0, 0, data.frame(a = 1, b = 2), layerId = "a", 70 | # legend = FALSE) 71 | # expect_null(map$legendArgs) 72 | # }) 73 | # }) 74 | # 75 | # describe("updateMinicharts", { 76 | # basemap <- leaflet::leaflet() %>% addMinicharts(0, 0, 1) 77 | # 78 | # it("correctly manages the 'labels' argument", { 79 | # # No labels 80 | # map <- basemap %>% updateMinicharts("a", showLabels = FALSE) 81 | # expect_equal(map$jsargs[[1]][[1]]$static$labels, "none") 82 | # # Auto labels 83 | # map <- basemap %>% updateMinicharts("a", showLabels = TRUE) 84 | # expect_equal(map$jsargs[[1]][[1]]$static$labels, "auto") 85 | # # Custom labels 86 | # map <- basemap %>% 87 | # updateMinicharts("a", showLabels = TRUE, labelText = "test") 88 | # expect_equal(map$jsargs[[1]][[1]]$static$labels, "test") 89 | # }) 90 | # 91 | # it ("does not update labels by default", { 92 | # map <- basemap %>% updateMinicharts("a") 93 | # expect_null(map$jsargs[[1]][[1]]$static$labels) 94 | # }) 95 | # 96 | # it ("updates time slider only when data is updated", { 97 | # map <- basemap %>% updateMinicharts("a") 98 | # expect_null(map$jsargs[[5]]) 99 | # 100 | # map <- basemap %>% updateMinicharts("a", chartdata = 2) 101 | # expect_false(is.null(map$jsargs[[5]])) 102 | # }) 103 | # 104 | # it ("updates legend only when data is updated", { 105 | # baseMap <- leaflet::leaflet() %>% addMinicharts(0, 0, 1, layerId = "a") 106 | # 107 | # map <- baseMap %>% updateMinicharts("a", data.frame(a = 1, b = 2)) 108 | # expect_false(is.null(map$legendArgs)) 109 | # expect_equal(map$legendArgs$labels, I(c("a", "b"))) 110 | # # No names, no legend 111 | # map <- baseMap %>% updateMinicharts("a", 1:3) 112 | # expect_null(map$legendArgs) 113 | # expect_equal(map$controlRemoved, "minichartsLegend") 114 | # # One column, no legend 115 | # map <- baseMap %>% updateMinicharts("a", data.frame(a = 1)) 116 | # expect_null(map$legendArgs) 117 | # expect_equal(map$controlRemoved, "minichartsLegend") 118 | # # Argument legend is FALSE 119 | # map <- baseMap %>% updateMinicharts("a", data.frame(a = 1, b = 2), legend = FALSE) 120 | # expect_null(map$legendArgs) 121 | # expect_equal(map$controlRemoved, "minichartsLegend") 122 | # 123 | # # data is not updated 124 | # map <- baseMap %>% updateMinicharts("a") 125 | # expect_null(map$legendArgs) 126 | # expect_null(map$controlRemoved) 127 | # }) 128 | # 129 | # }) 130 | # 131 | # } 132 | # ) 133 | -------------------------------------------------------------------------------- /javascript/utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var d3 = require("d3"); 5 | 6 | module.exports.initTimeSlider = initTimeSlider; 7 | module.exports.processOptions = processOptions; 8 | module.exports.addSetTimeIdMethod = addSetTimeIdMethod; 9 | module.exports.addRemoveMethods = addRemoveMethods; 10 | module.exports.getInitOptions = getInitOptions; 11 | module.exports.setPopup = setPopup; 12 | 13 | // Add a time slider if it does not already exist 14 | function initTimeSlider(map, timeLabels, initialTime) { 15 | var tslider; 16 | 17 | if (!map.controls._controlsById.tslider) { 18 | tslider = L.timeSlider({ 19 | timeLabels: timeLabels, 20 | onTimeIdChange: function(timeId) { 21 | var types = ["minichart", "flow"]; 22 | for (var i = 0; i < types.length; i++) { 23 | var layers = map.layerManager._byCategory[types[i]]; 24 | for (var k in layers) { 25 | if (layers[k]) layers[k].setTimeId(timeId); 26 | } 27 | } 28 | } 29 | }); 30 | map.controls.add(tslider, "tslider"); 31 | } else { 32 | tslider = map.controls._controlsById.tslider; 33 | if (typeof timeLabels != "undefined") tslider.setTimeLabels(timeLabels); 34 | } 35 | 36 | var timeId; 37 | if (typeof initialTime != "undefined" && initialTime !== null) { 38 | timeId = tslider.toTimeId(initialTime); 39 | } else { 40 | timeId = tslider.getTimeId(); 41 | } 42 | tslider.setTimeId(timeId); 43 | 44 | return timeId; 45 | } 46 | 47 | /* Convert options sent by R in a useful format for the javascript code. 48 | Args: 49 | - options: object sent by R 50 | - callback: function(opts, i) with 'opts' the options for a given layer 51 | and 'i' the index of the given layer 52 | */ 53 | function processOptions(options, callback) { 54 | for (var i = 0; i < options.length; i++) { 55 | var opts = []; 56 | var timesteps = getNumberOfTimesteps(options); 57 | 58 | for (var t = 0; t < timesteps; t++) { 59 | var opt = {}; 60 | for (var k in options[i].dyn) { 61 | if (options[i].dyn.hasOwnProperty(k)) { 62 | opt[k] = options[i].dyn[k][t]; 63 | } 64 | } 65 | 66 | opts.push(opt); 67 | } 68 | 69 | callback(opts, i, options[i].static); 70 | } 71 | } 72 | 73 | function getNumberOfTimesteps(options) { 74 | return options[0].timeSteps; 75 | } 76 | 77 | function getInitOptions(opts, staticOpts, timeId) { 78 | var opt = opts[timeId]; 79 | for (var k in opt) { 80 | if (opt.hasOwnProperty(k)) { 81 | staticOpts[k] = opt[k]; 82 | } 83 | } 84 | return staticOpts; 85 | } 86 | 87 | // Add to a leaflet class the method "setTimeId" to update a layer when timeId 88 | // changes. 89 | function addSetTimeIdMethod(className, updateFunName) { 90 | if (!L[className].prototype.setTimeId) { 91 | L[className].prototype.setTimeId = function(timeId) { 92 | if (timeId == this.timeId) return; 93 | 94 | if (typeof this.opts !== "undefined" && typeof this.opts[timeId] !== 'undefined') { 95 | var opt = this.opts[timeId]; 96 | this[updateFunName](opt); 97 | var popup = setPopup(this, timeId); 98 | if (this.onChange) this.onChange(opt, popup, d3); 99 | } 100 | this.timeId = timeId; 101 | }; 102 | } 103 | } 104 | 105 | function addRemoveMethods(className, groupName) { 106 | LeafletWidget.methods["remove" + className + "s"] = function(layerId) { 107 | if (layerId.constructor != Array) layerId = [layerId]; 108 | for (var i = 0; i < layerId.length; i++) { 109 | this.layerManager.removeLayer(groupName, layerId[i]); 110 | } 111 | }; 112 | 113 | LeafletWidget.methods["clear" + className + "s"] = function() { 114 | this.layerManager.clearLayers(groupName); 115 | }; 116 | } 117 | 118 | function setPopup(l, timeId) { 119 | if (l.popupArgs.noPopup) return; 120 | 121 | if (l.opts[timeId].popupHTML) { 122 | if(l.isPopupOpen() === false){ 123 | l.bindPopup(l.opts[timeId].popupHTML, l.popupOptions); 124 | } else { 125 | l.setPopupContent(l.opts[timeId].popupHTML); 126 | } 127 | /* l.bindPopup(l.opts[timeId].popupHTML); */ 128 | return; 129 | } 130 | 131 | var title, content, popup; 132 | if (l.layerId && l.popupArgs.showTitle) title = "

" + l.layerId + "

"; 133 | else title = ""; 134 | content = ""; 135 | if (l.opts[timeId].data) { 136 | var values, keys; 137 | if(l.popupArgs.showValues) { 138 | values = l.opts[timeId].data; 139 | keys = l.popupArgs.labels.concat(l.popupArgs.supLabels); 140 | } else { 141 | values = []; 142 | keys = l.popupArgs.supLabels; 143 | } 144 | 145 | if (l.opts[timeId].popupData) values = values.concat(l.opts[timeId].popupData); 146 | 147 | if (keys.length == 0) { 148 | content = values.join(", "); 149 | } else { 150 | var rows = []; 151 | for (var i = 0; i < values.length; i++) { 152 | var row = ""; 153 | if (l.popupArgs.digits && isNumeric(values[i])) { 154 | values[i] = parseFloat(parseFloat(values[i]).toFixed(l.popupArgs.digits)); 155 | } 156 | row += "" + keys[i] + ""; 157 | row += "" + values[i] + ""; 158 | row = "" + row + ""; 159 | rows.push(row); 160 | } 161 | content = rows.join(""); 162 | content = '' + content +'
'; 163 | } 164 | } 165 | 166 | popup = ''; 167 | /* l.bindPopup(popup); */ 168 | if(l.isPopupOpen() === false){ 169 | l.bindPopup(popup, l.popupOptions); 170 | } else { 171 | l.setPopupContent(popup); 172 | } 173 | return popup; 174 | } 175 | 176 | function isNumeric(n) { 177 | return !isNaN(parseFloat(n)) && isFinite(n); 178 | } 179 | }()); 180 | -------------------------------------------------------------------------------- /R/prepare_js_args.R: -------------------------------------------------------------------------------- 1 | #' Prepare arguments before sending them to the javascript program. 2 | #' 3 | #' Globally the function reorder data by layerId and time and then it split it 4 | #' by layerId. It also compute some useful values like labels for legend, 5 | #' domain of data, number of data columns. 6 | #' 7 | #' @param options Object created with .preprocessArgs() 8 | #' @param chartdata Data to represent with minicharts 9 | #' @param popupArgs object created with function popupArgs() 10 | #' @param onChange see addMinicharts() 11 | #' 12 | #' @return 13 | #' A list with the following elements: 14 | #' - options: graphical options, split by layerId. Each element contains 15 | #' elements "dyn" (data.frame with values that vary with time), "static" 16 | #' and "timeSteps" (number of time steps) 17 | #' - chartdata: numeric matrix split by layerId 18 | #' - maxValues: maximal value observed in chartdata (NULL if chartdata is NULL) 19 | #' - ncols: number of columns in chartdata 20 | #' - popupArgs: List containing additional data to display in popups (or the 21 | #' popup HTML) split by layerId 22 | #' - legendLab: Labels to use in legends 23 | #' - onChange: Javascript function that will be executed when charts are 24 | #' updated 25 | #' 26 | #' @noRd 27 | #' 28 | .prepareJSArgs <- function(options, chartdata = NULL, popupArgs = NULL, 29 | onChange = NULL, timeFormat = NULL, 30 | initialTime = NULL) { 31 | 32 | static <- c("layerId", "lat", "lat0", "lat1", "lng", "lng0", "lng1") 33 | 34 | staticOptions <- options$staticOptions 35 | options <- options$options 36 | time <- options$time 37 | 38 | correctOrder <- order(options$layerId, options$time) 39 | 40 | options <- options[correctOrder, ] 41 | 42 | if (is.null(chartdata)) { 43 | legendLab <- NULL 44 | maxValues <- NULL 45 | ncols <- 0 46 | } else { 47 | # When adding only one minichart, chartdata can be a vector or a data frame, so it 48 | # needs to be converted to a matrix with correct lines and columns 49 | if (nrow(options) == 1 && is.vector(chartdata)) { 50 | chartdata <- matrix(chartdata, nrow = 1) 51 | } else { 52 | if (is.vector(chartdata)) { 53 | chartdata <- matrix(chartdata, ncol = 1, nrow = nrow(options)) 54 | } 55 | } 56 | 57 | # Save column names for legend and transform data in a matrix without names 58 | if (!is.null(popupArgs)) { 59 | if (is.null(popupArgs$labels)) popupArgs$labels <- colnames(chartdata) 60 | legendLab <- popupArgs$labels 61 | } else { 62 | legendLab <- colnames(chartdata) 63 | } 64 | chartdata <- unname(as.matrix(chartdata)) 65 | 66 | # Save additional information about data before splitting it 67 | maxValues <- max(abs(chartdata)) 68 | ncols <- ncol(chartdata) 69 | 70 | # sort data and split it by layer 71 | chartdata <- chartdata[correctOrder, ] %>% 72 | split(options$layerId, drop = TRUE) %>% 73 | lapply(matrix, ncol = ncols) %>% 74 | unname() 75 | } 76 | 77 | # Popup additional data 78 | if (!is.null(popupArgs$supValues)) { 79 | if (is.null(popupArgs$supLabels)) popupArgs$supLabels <- colnames(popupArgs$supValues) 80 | 81 | if (is.null(popupArgs$supLabels) && !is.null(popupArgs$labels)) { 82 | popupArgs$supLabels <- rep("", ncol(popupArgs$supValues)) 83 | } else if (is.null(popupArgs$labels) && !is.null(popupArgs$supLabels)) { 84 | popupArgs$labels <-rep("", ncols) 85 | } 86 | 87 | popupArgs$supValues <- popupArgs$supValues[correctOrder, ] %>% 88 | as.matrix() %>% 89 | split(options$layerId, drop = TRUE) %>% 90 | lapply(matrix, ncol = ncol(popupArgs$supValues)) %>% 91 | unname() 92 | } 93 | 94 | # Popup html 95 | if (!is.null(popupArgs$html)) { 96 | popupArgs$html <- popupArgs$html[correctOrder] %>% 97 | as.character() %>% 98 | split(options$layerId, drop = TRUE) %>% 99 | lapply(.I) %>% 100 | unname() 101 | } 102 | 103 | 104 | # If there is only one variable in chartdata, we draw circles with different radius 105 | # else we draw bar charts by default. 106 | if ("type" %in% names(staticOptions) && staticOptions$type == "auto") { 107 | staticOptions$type <- ifelse (!is.null(ncols) && ncols == 1, "polar-area", "bar") 108 | } 109 | 110 | # Ensure layerId is a character vector 111 | if ("layerId" %in% names(options)) options$layerId <- as.character(options$layerId) 112 | 113 | # Finally split options by layer 114 | options <- split(options, options$layerId, drop = TRUE) %>% 115 | unname() %>% 116 | lapply(function(df) { 117 | df$time <- NULL 118 | res <- list(dyn = df, static = list(), timeSteps = nrow(df)) 119 | # Add common static options 120 | for (var in names(staticOptions)) { 121 | res$static[[var]] <- staticOptions[[var]] 122 | } 123 | # Add individual static options 124 | for (var in static) { 125 | if (var %in% names(df)) { 126 | res$dyn[[var]] <- NULL 127 | res$static[[var]] <- df[[var]][1] 128 | } 129 | } 130 | res 131 | }) 132 | 133 | # Ensure labels will always be translated as arrays in JSON 134 | if(!is.null(popupArgs)) { 135 | popupArgs$labels <- .I(popupArgs$labels) 136 | popupArgs$supLabels <- .I(popupArgs$supLabels) 137 | } 138 | 139 | # Prepare onChange argument 140 | if (!is.null(onChange)) { 141 | onChange <- sprintf("(function(opts, popup, d3){%s})", onChange) 142 | onChange <- JS(onChange) 143 | } 144 | 145 | # Prepare time labels 146 | timeLabels <- sort(unique(time)) 147 | if (!is.null(timeFormat)) { 148 | timeLabels <- format(timeLabels, format = timeFormat) 149 | if (!is.null(initialTime)) { 150 | initialTime <- format(initialTime, format = timeFormat) 151 | } 152 | } 153 | timeLabels <- as.character(timeLabels) 154 | if (!is.null(initialTime)) initialTime <- as.character(initialTime) 155 | 156 | list( 157 | options = options, 158 | chartdata = chartdata, 159 | maxValues = maxValues, 160 | ncols = ncols, 161 | popupArgs = popupArgs, 162 | legendLab = .I(legendLab), 163 | onChange = onChange, 164 | timeLabels = .I(timeLabels), 165 | initialTime = initialTime 166 | ) 167 | } 168 | 169 | 170 | .I <- function(x) { 171 | if(is.null(x)) x <- list() 172 | I(x) 173 | } 174 | -------------------------------------------------------------------------------- /tests/testthat/test-prepare_js_args.R: -------------------------------------------------------------------------------- 1 | context(".prepareJSArgs") 2 | 3 | describe(".prepareJSArgs", { 4 | 5 | # Prepare some fake data 6 | nTimeIds <- 5 7 | layerIds <- c("a", "b") 8 | nLayers <- length(layerIds) 9 | 10 | mydata <- data.frame( 11 | a = rnorm(nLayers * nTimeIds), 12 | b = rnorm(nLayers * nTimeIds) 13 | ) 14 | 15 | myOptions <- .preprocessArgs( 16 | list( 17 | lng = rep(1:nLayers, each = nTimeIds), 18 | lat = rep(1:nLayers, each = nTimeIds), 19 | layerId = rep(layerIds, each = nTimeIds), 20 | time = rep(1:nTimeIds, each = nLayers) 21 | ), 22 | list( 23 | width = 60, 24 | height = seq_len(nLayers * nTimeIds) 25 | ) 26 | ) 27 | 28 | jsArgs <- .prepareJSArgs(myOptions, mydata) 29 | 30 | # Helper functions 31 | expect_split_by_layer <- function(x, checkElement) { 32 | expect_is(x, "list") 33 | expect_equal(length(x), nLayers) 34 | for (el in x) { 35 | checkElement(el) 36 | } 37 | } 38 | 39 | it("returns an object with the correct structure", { 40 | elements <- c("options", "chartdata", "maxValues", "ncols", "legendLab", 41 | "onChange", "timeLabels", "initialTime") 42 | expect_true(all(elements %in% names(jsArgs))) 43 | expect_is(jsArgs$legendLab, "AsIs") 44 | expect_equal(jsArgs$legendLab, I(colnames(mydata))) 45 | expect_equal(jsArgs$maxValues, max(abs(mydata))) 46 | expect_equal(jsArgs$ncols, ncol(mydata)) 47 | expect_is(jsArgs$timeLabels, "AsIs") 48 | 49 | expect_split_by_layer(jsArgs$options, function(el) { 50 | expect_is(el$dyn, "data.frame") 51 | expect_equal(names(el$dyn), "height") 52 | expect_equal(nrow(el$dyn), nTimeIds) 53 | 54 | expect_is(el$static, "list") 55 | expect_true(all(c("lng", "lat", "layerId", "width") %in% names(el$static))) 56 | 57 | expect_equal(el$timeSteps, nTimeIds) 58 | }) 59 | 60 | expect_split_by_layer(jsArgs$chartdata, function(el) { 61 | expect_is(el, "matrix") 62 | expect_equal(mode(el), "numeric") 63 | expect_equal(dim(el), c(nTimeIds, ncol(mydata))) 64 | }) 65 | }) 66 | 67 | it ("handles case when chartdata is a vector", { 68 | it ("single timeId and layer", { 69 | singleOption <- list(options = myOptions$options[1,]) 70 | myData <- 1:3 71 | jsArgs <- .prepareJSArgs(singleOption, myData) 72 | expect_equal(length(jsArgs$chartdata), 1) 73 | expect_equal(dim(jsArgs$chartdata[[1]]), c(1, length(myData))) 74 | }) 75 | 76 | it ("single column", { 77 | myData <- 1:nrow(myOptions$options) 78 | jsArgs <- .prepareJSArgs(myOptions, myData) 79 | expect_split_by_layer(jsArgs$chartdata, function(el) { 80 | expect_is(el, "matrix") 81 | expect_equal(mode(el), "numeric") 82 | expect_equal(dim(el), c(nTimeIds, 1)) 83 | }) 84 | }) 85 | 86 | it ("single value", { 87 | myData <- 1 88 | jsArgs <- .prepareJSArgs(myOptions, myData) 89 | expect_split_by_layer(jsArgs$chartdata, function(el) { 90 | expect_is(el, "matrix") 91 | expect_equal(mode(el), "numeric") 92 | expect_equal(dim(el), c(nTimeIds, 1)) 93 | expect_true(all(el == myData)) 94 | }) 95 | }) 96 | }) 97 | 98 | it ("uses default values when chartdata is NULL", { 99 | expect_silent(jsArgs <- .prepareJSArgs(myOptions, NULL)) 100 | expect_equal(jsArgs$legendLab, I(list())) 101 | expect_equal(jsArgs$maxValues, NULL) 102 | expect_equal(jsArgs$ncols, 0) 103 | }) 104 | 105 | it ("chooses correct type when type = 'auto'", { 106 | 107 | it ("multiple columns", { 108 | myOptions$staticOptions$type <- "auto" 109 | jsArgs <- .prepareJSArgs(myOptions, mydata) 110 | expect_split_by_layer(jsArgs$options, function(el) { 111 | expect_equal(el$static$type, "bar") 112 | }) 113 | }) 114 | 115 | it ("single column", { 116 | myOptions$staticOptions$type <- "auto" 117 | jsArgs <- .prepareJSArgs(myOptions, mydata[, "a", drop = FALSE]) 118 | expect_split_by_layer(jsArgs$options, function(el) { 119 | expect_equal(el$static$type, "polar-area") 120 | }) 121 | }) 122 | 123 | 124 | }) 125 | 126 | it ("transforms 'onChange' in a javascript function", { 127 | jsArgs <- .prepareJSArgs(myOptions, onChange = "console.log('ok')") 128 | expect_is(jsArgs$onChange, "JS_EVAL") 129 | expect_true(grepl("^\\(function\\(.+\\) ?\\{.+\\}\\)", jsArgs$onChange)) 130 | }) 131 | 132 | it ("supports custom popups", { 133 | 134 | it ("custom html", { 135 | html <- rnorm(nLayers * nTimeIds) 136 | 137 | jsArgs <- .prepareJSArgs(myOptions, mydata, popupArgs(html = html)) 138 | expect_split_by_layer(jsArgs$popupArgs$html, function(el) { 139 | expect_is(el, "AsIs") 140 | expect_equal(mode(el), "character") 141 | expect_equal(length(el), nTimeIds) 142 | }) 143 | }) 144 | 145 | it ("custom labels", { 146 | jsArgs <- .prepareJSArgs(myOptions, mydata, popupArgs(labels = c("c", "d"))) 147 | expect_equal(jsArgs$legendLab, I(c("c", "d"))) 148 | }) 149 | 150 | it ("additional data", { 151 | popupData <- data.frame( 152 | c = rnorm(nLayers * nTimeIds), 153 | d = rnorm(nLayers * nTimeIds) 154 | ) 155 | 156 | jsArgs <- .prepareJSArgs(myOptions, mydata, 157 | popupArgs(supValues = popupData)) 158 | 159 | expect_split_by_layer(jsArgs$popupArgs$supValues, function(el) { 160 | expect_is(el, "matrix") 161 | expect_equal(mode(el), "numeric") 162 | expect_equal(dim(el), c(nTimeIds, ncol(mydata))) 163 | }) 164 | expect_is(jsArgs$popupArgs$supLabels, "AsIs") 165 | expect_equal(jsArgs$popupArgs$supLabels, I(names(popupData))) 166 | }) 167 | }) 168 | 169 | it ("fills missing labels", { 170 | popupData <- data.frame( 171 | c = rnorm(nLayers * nTimeIds), 172 | d = rnorm(nLayers * nTimeIds) 173 | ) 174 | 175 | jsArgs <- .prepareJSArgs(myOptions, mydata, 176 | popupArgs(supValues = unname(popupData))) 177 | expect_equal(jsArgs$popupArgs$labels, I(names(mydata))) 178 | expect_equal(jsArgs$popupArgs$supLabels, I(c("", ""))) 179 | 180 | jsArgs <- .prepareJSArgs(myOptions, unname(mydata), 181 | popupArgs(supValues = popupData)) 182 | expect_equal(jsArgs$popupArgs$labels, I(c("", ""))) 183 | expect_equal(jsArgs$popupArgs$supLabels, I(names(popupData))) 184 | }) 185 | 186 | it ("can use custom format for time labels", { 187 | myOptions$options$time <- Sys.Date() + myOptions$options$time 188 | format <- "%m - %d" 189 | expectedLabels <- format(unique(myOptions$options$time), format = format) 190 | 191 | jsArgs <- .prepareJSArgs(myOptions, mydata, timeFormat = format, 192 | initialTime = min(myOptions$options$time)) 193 | expect_equal(jsArgs$timeLabels, I(expectedLabels)) 194 | expect_equal(jsArgs$initialTime, expectedLabels[1]) 195 | }) 196 | 197 | it ("returns timeLabels and initialTimes in character format", { 198 | jsArgs <- .prepareJSArgs(myOptions, mydata, 199 | initialTime = min(myOptions$options$time)) 200 | expect_equal(mode(jsArgs$timeLabels), "character") 201 | expect_is(jsArgs$initialTime, "character") 202 | }) 203 | }) 204 | -------------------------------------------------------------------------------- /vignettes/introduction.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction to leaflet.minicharts" 3 | author: "Francois Guillem" 4 | date: "`r Sys.Date()`" 5 | output: rmarkdown::html_vignette 6 | vignette: > 7 | %\VignetteIndexEntry{introduction} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | %\VignetteEncoding{UTF-8} 10 | --- 11 | 12 | For a few years now, it has become very to create interactive maps with R thanks to the package `leaflet` by the Rstudio team. Nevertheless, it only provides only a few functions to create basic shapes on a map, so the information that can be represented on a single map is limited: if you have some data associated to some points, you can only represent at most two variables by drawing circles and changing their radius and color according to data. 13 | 14 | `leaflet.minicharts` is an R package that provides two functions to add and update small charts on an interactive maps created with the package `leaflet`. These charts can be used to represent as many variables as desired associated to geographical points. Currently, three types of chart are supported: barcharts (the default), pie charts and polar area charts. 15 | 16 | let's have a look to a concrete example. 17 | 18 | ## Data 19 | 20 | The package provides a table that contains the electric production, consumption and exchanges of France from january 2010 to february 2017 and of 12 french regions from january 2013 to february 2017. 21 | 22 | In addition to the total production, the table contains one column for each type of production. The table also contains the latitude and longitude of the center of the regions. 23 | 24 | ```{r} 25 | library(leaflet.minicharts) 26 | data("eco2mix") 27 | head(eco2mix) 28 | ``` 29 | 30 | ## Renewable productions in 2016 31 | 32 | Nowadays, France has an objective of 23% of renewable energies in the consumption of the country by 2020. Are the country close to its objective. Is the share of renewable energies similar in all regions? 33 | 34 | To answer this question let us focus on the year 2016 We first prepare the required data with package `dplyr`: 35 | 36 | ```{r message=FALSE} 37 | library(dplyr) 38 | 39 | prod2016 <- eco2mix %>% 40 | mutate( 41 | renewable = bioenergy + solar + wind + hydraulic, 42 | non_renewable = total - bioenergy - solar - wind - hydraulic 43 | ) %>% 44 | filter(grepl("2016", month) & area != "France") %>% 45 | select(-month) %>% 46 | group_by(area, lat, lng) %>% 47 | summarise_all(sum) %>% 48 | ungroup() 49 | 50 | head(prod2016) 51 | ``` 52 | 53 | We also create a base map that will be used in all the following examples 54 | 55 | ```{r message=FALSE, results='hide'} 56 | library(leaflet) 57 | 58 | tilesURL <- "http://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}" 59 | 60 | basemap <- leaflet(width = "100%", height = "400px") %>% 61 | addTiles(tilesURL) 62 | ``` 63 | 64 | We now add to the base map a pie chart for each region that represents the share of renewable energies. We also change the width of the pie charts so their area is proportional to the total production of the corresponding region. 65 | 66 | ```{r} 67 | colors <- c("#4fc13c", "#cccccc") 68 | 69 | basemap %>% 70 | addMinicharts( 71 | prod2016$lng, prod2016$lat, 72 | type = "pie", 73 | chartdata = prod2016[, c("renewable", "non_renewable")], 74 | colorPalette = colors, 75 | width = 60 * sqrt(prod2016$total) / sqrt(max(prod2016$total)), transitionTime = 0 76 | ) 77 | ``` 78 | 79 | We can see that the three south east regions exceed the target of 23%, but most regions are far from this objective. Globally, renewable energies represented only 19% percent of the production of 2016. 80 | 81 | Now let's represent the different types of renewable production using bar charts. 82 | 83 | ```{r} 84 | renewable2016 <- prod2016 %>% select(hydraulic, solar, wind) 85 | colors <- c("#3093e5", "#fcba50", "#a0d9e8") 86 | basemap %>% 87 | addMinicharts( 88 | prod2016$lng, prod2016$lat, 89 | chartdata = renewable2016, 90 | colorPalette = colors, 91 | width = 45, height = 45 92 | ) 93 | ``` 94 | 95 | Hydraulic production is far more important than solar and wind. Without surprise, solar production is more important in south while wind production is more important in the north. 96 | 97 | ## Representing a single variable 98 | 99 | `leaflet.minicharts` has been designed to represent multiple variables at once, but you still may want to use it to represent a single variable. In the next example, we represent the total load of each french region in 2016. When data passed to `addMinicharts` contains a single column, it automatically represents it with circle which area is proportional to the corresponding value. In the example we also use the parameter `showLabels` to display rounded values of the variable inside the circles. 100 | 101 | ```{r} 102 | basemap %>% 103 | addMinicharts( 104 | prod2016$lng, prod2016$lat, 105 | chartdata = prod2016$load, 106 | showLabels = TRUE, 107 | width = 45 108 | ) 109 | ``` 110 | 111 | This is nice, isn't it? 112 | 113 | ## Animated maps 114 | 115 | Until now, we have only represented aggregated data but it would be nice to create a map that represents the evolution over time of some variables. It is actually easy with `leaflet.minicharts`. The first step is to construct a table containing longitude, latitude, a time column and the variables we want to represent. The table `eco2mix` already has all these columns. We only need to filter the rows containing data for the entire country. 116 | 117 | ```{r} 118 | prodRegions <- eco2mix %>% filter(area != "France") 119 | ``` 120 | 121 | Now we can create our animated map by using the argument "time": 122 | 123 | ```{r} 124 | basemap %>% 125 | addMinicharts( 126 | prodRegions$lng, prodRegions$lat, 127 | chartdata = prodRegions[, c("hydraulic", "solar", "wind")], 128 | time = prodRegions$month, 129 | colorPalette = colors, 130 | width = 45, height = 45 131 | ) 132 | ``` 133 | 134 | ## Represent flows 135 | 136 | Since version 0.2, `leaflet.minicharts` has also functions to represent flows between points and their evolution. To illustrate this, let's represent the evolution of electricity exchanges between France and Neighboring countries. 137 | 138 | To do that, we use function `addFlows`. It requires coordinates of two points for each flow and the value of the flow. Other arguments are similar to `addMinicharts`. 139 | 140 | ```{r} 141 | data("eco2mixBalance") 142 | bal <- eco2mixBalance 143 | basemap %>% 144 | addFlows( 145 | bal$lng0, bal$lat0, bal$lng1, bal$lat1, 146 | flow = bal$balance, 147 | time = bal$month 148 | ) 149 | ``` 150 | 151 | Of course, you can represent flows and minicharts on the same map! 152 | 153 | ## Use in shiny web applications 154 | 155 | In shiny applications, you can create nice transition effects by using functions `leafletproxy` and `updateMinicharts`/`updateFlows`. In the server function you first need to initialize the map and the minicharts. The important thing here is to use parameter `layerId` so that `updateMinicharts` can know which chart to update with which values. 156 | 157 | ```{r eval = FALSE} 158 | server <- function(input, output, session) { 159 | # Initialize map 160 | output$mymap <- renderLeaflet( 161 | leaflet() %>% addTiles() %>% 162 | addMinicharts(lon, lat, layerId = uniqueChartIds) 163 | ) 164 | } 165 | ``` 166 | 167 | Then use `leafletProxy()` and `updateMinicharts` in your reactive code: 168 | 169 | ```{r eval = FALSE} 170 | server <- function(input, output, session) { 171 | # Initialize map 172 | ... 173 | 174 | # Update map 175 | observe({ 176 | newdata <- getData(input$myinput) 177 | 178 | leafletProxy("mymap") %>% 179 | updateMinicharts(uniqueChartIds, chartdata = newdata, ...) 180 | }) 181 | } 182 | ``` 183 | 184 | You can find a [live example here](https://francoisguillem.shinyapps.io/shiny-demo/). 185 | -------------------------------------------------------------------------------- /man/addMinicharts.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/add_minicharts.R 3 | \name{addMinicharts} 4 | \alias{addMinicharts} 5 | \alias{updateMinicharts} 6 | \alias{removeMinicharts} 7 | \alias{clearMinicharts} 8 | \title{Add or update charts on a leaflet map} 9 | \usage{ 10 | addMinicharts( 11 | map, 12 | lng, 13 | lat, 14 | chartdata = 1, 15 | time = NULL, 16 | maxValues = NULL, 17 | type = "auto", 18 | fillColor = d3.schemeCategory10[1], 19 | colorPalette = d3.schemeCategory10, 20 | width = 30, 21 | height = 30, 22 | opacity = 1, 23 | showLabels = FALSE, 24 | labelText = NULL, 25 | labelMinSize = 8, 26 | labelMaxSize = 24, 27 | labelStyle = NULL, 28 | transitionTime = 750, 29 | popup = popupArgs(), 30 | layerId = NULL, 31 | legend = TRUE, 32 | legendPosition = "topright", 33 | timeFormat = NULL, 34 | initialTime = NULL, 35 | onChange = NULL, 36 | popupOptions = NULL 37 | ) 38 | 39 | updateMinicharts( 40 | map, 41 | layerId, 42 | chartdata = NULL, 43 | time = NULL, 44 | maxValues = NULL, 45 | type = NULL, 46 | fillColor = NULL, 47 | colorPalette = d3.schemeCategory10, 48 | width = NULL, 49 | height = NULL, 50 | opacity = NULL, 51 | showLabels = NULL, 52 | labelText = NULL, 53 | labelMinSize = NULL, 54 | labelMaxSize = NULL, 55 | labelStyle = NULL, 56 | transitionTime = NULL, 57 | popup = NULL, 58 | legend = TRUE, 59 | legendPosition = NULL, 60 | timeFormat = NULL, 61 | initialTime = NULL, 62 | onChange = NULL, 63 | popupOptions = NULL 64 | ) 65 | 66 | removeMinicharts(map, layerId) 67 | 68 | clearMinicharts(map) 69 | } 70 | \arguments{ 71 | \item{map}{A leaflet map object created with \code{\link[leaflet]{leaflet}}.} 72 | 73 | \item{lng}{Longitude where to place the charts.} 74 | 75 | \item{lat}{Latitude where to place the charts.} 76 | 77 | \item{chartdata}{A numeric matrix with number of rows equal to the number of 78 | elements in \code{lng} or \code{lat} and number of column equal to the 79 | number of variables to represent. If parameter \code{time} is set, the 80 | number of rows must be equal to the length of \code{lng} times the number 81 | of unique time steps in the data.} 82 | 83 | \item{time}{A vector with length equal to the number of rows in \code{chartdata} 84 | and containing either numbers representing time indices or dates or 85 | datetimes. Each unique value must appear as many times as the others. This 86 | parameter can be used when one wants to represent the evolution of some 87 | variables on a map.} 88 | 89 | \item{maxValues}{maximal absolute values of the variables to represent. 90 | It can be a vector with one value per column of \code{chartdata} or a single 91 | value. Using a single value enforces charts to use a unique scale for all 92 | variables. If it is \code{NULL}, the maximum value of \code{chartdata} is used.} 93 | 94 | \item{type}{Type of chart. Possible values are \code{"bar"} for bar charts, 95 | \code{"pie"} for pie charts, \code{"polar-area"} and \code{"polar-radius"} 96 | for polar area charts where the values are represented respectively by the 97 | area or the radius of the slices. Finally it can be equal to \code{"auto"}, 98 | the default. In this case, if there is only one variable to represent, the 99 | chart will be a single circle, else it is a barchart.} 100 | 101 | \item{fillColor}{Used only if data contains only one column. It is the color 102 | used to fill the circles.} 103 | 104 | \item{colorPalette}{Color palette to use when \code{chartdata} contains more than 105 | one column.} 106 | 107 | \item{width}{maximal width of the created elements.} 108 | 109 | \item{height}{maximal height of the created elements.} 110 | 111 | \item{opacity}{Opacity of the chart.} 112 | 113 | \item{showLabels}{Should values be displayed above chart elements.} 114 | 115 | \item{labelText}{character vector containing the text content of the charts. 116 | Used only if \code{chartdata} contains only one column.} 117 | 118 | \item{labelMinSize}{Minimal height of labels in pixels. When there is not 119 | enough space for labels, they are hidden.} 120 | 121 | \item{labelMaxSize}{Maximal height of labels in pixels.} 122 | 123 | \item{labelStyle}{Character string containing CSS properties to apply to the 124 | labels.} 125 | 126 | \item{transitionTime}{Duration in milliseconds of the transitions when a 127 | property of a chart is updated.} 128 | 129 | \item{popup}{Options that control popup generation.} 130 | 131 | \item{layerId}{An ID variable. It is mandatory when one wants to update some 132 | chart with \code{updateMinicharts}.} 133 | 134 | \item{legend}{If TRUE and if data has column names, then a legend is 135 | automatically added to the map.} 136 | 137 | \item{legendPosition}{Where should legend be placed?} 138 | 139 | \item{timeFormat}{Character string used to format dates and times when 140 | argument \code{time} is a \code{Date}, \code{POSIXct} or \code{POSIXlt} 141 | object. See \code{\link[base]{strptime}} for more information.} 142 | 143 | \item{initialTime}{This argument can be used to set the initial time step 144 | shown when the map is created. It is used only when argument \code{time} is 145 | set.} 146 | 147 | \item{onChange}{(For power users who know javascript) A character string 148 | containing javascript code that is executed each time a chart is updated. 149 | See the details section to understand why and how to use this parameter.} 150 | 151 | \item{popupOptions}{Change default popupOptions (ex : autoClose, maxHeight, closeButton ...) 152 | See \code{\link[leaflet]{popupOptions}} for more informations.} 153 | } 154 | \value{ 155 | The modified leaflet map object. \code{addMinicharts} add new minicharts to 156 | the map. \code{updateMinicharts} updates minicharts that have already been 157 | added to the map. \code{removeMinicharts} removes some specific charts from 158 | the map and \code{clearMinicharts} removes all charts from the map and 159 | if necessary the legend that has been automatically created. 160 | } 161 | \description{ 162 | these functions add or update minicharts in a leaflet map at given coordinates: 163 | they can be bar charts, pie charts or polar charts where chartdata is encoded 164 | either by area or by radius. 165 | } 166 | \details{ 167 | Since version 0.5, the parameter \code{onChange} can be used to execute 168 | some arbitrary javascript code each time a chart is updated (with 169 | \code{updateMinicharts()} or when time step changes). A typical use case 170 | would be to change the color of a polygon added with 171 | \code{\link[leaflet]{addPolygons}} based on the data of the chart. It is even 172 | possible to create an invisible chart and use it to manage the color and the 173 | popup of a polygon. Here is a sample code that do that: 174 | 175 | \preformatted{ 176 | leaflet() \%>\% addTiles() \%>\% 177 | addPolygons(data = myPolygons, layerId = myPolygons$myIds) \%>\% 178 | addMinicharts( 179 | mydata$lon, mydata$lat, 180 | time = mydata$time 181 | fillColor = mydata$color, 182 | layerId = mydata$myIds, 183 | width = 0, height = 0, 184 | onChange = " 185 | var s = this._map.layerManager.getLayer("shape", this.layerId); 186 | s.bindPopup(popup); 187 | if (opts.fillColor) { 188 | d3.select(s._path) 189 | .transition() 190 | .duration(750) 191 | .attr("fill", opts.fillColor); 192 | }" 193 | ) 194 | 195 | } 196 | 197 | The following objects are available when executing the javascript code: 198 | \describe{ 199 | \item{this}{The current minichart object. See 200 | \url{https://rte-antares-rpackage.github.io/leaflet.minichart/-_L.Minichart_.html} 201 | for more information. 202 | } 203 | \item{opts}{The current options passed to the current minichart object.} 204 | \item{popup}{Popup html.} 205 | \item{d3}{The D3 module.} 206 | } 207 | 208 | Here is a toy example 209 | } 210 | \examples{ 211 | require(leaflet) 212 | mymap <- leaflet() \%>\% addTiles() \%>\% addMinicharts(0, 0, chartdata = 1:3, layerId = "c1") 213 | 214 | mymap 215 | mymap \%>\% updateMinicharts("c1", maxValues = 6) 216 | mymap \%>\% updateMinicharts("c1", type="pie") 217 | 218 | # popupOptions 219 | mymap <- leaflet() \%>\% addTiles() \%>\% 220 | addMinicharts(0, 0, chartdata = 1:3, layerId = "c1", popupOptions = list(closeButton = FALSE)) 221 | 222 | mymap 223 | mymap \%>\% updateMinicharts("c1", maxValues = 6, popupOptions = list(closeButton = TRUE)) 224 | 225 | } 226 | -------------------------------------------------------------------------------- /R/add_minicharts.R: -------------------------------------------------------------------------------- 1 | # Copyright © 2016 RTE Réseau de transport d’électricité 2 | 3 | #' Add or update charts on a leaflet map 4 | #' 5 | #' these functions add or update minicharts in a leaflet map at given coordinates: 6 | #' they can be bar charts, pie charts or polar charts where chartdata is encoded 7 | #' either by area or by radius. 8 | #' 9 | #' @param map A leaflet map object created with \code{\link[leaflet]{leaflet}}. 10 | #' @param lng Longitude where to place the charts. 11 | #' @param lat Latitude where to place the charts. 12 | #' @param chartdata A numeric matrix with number of rows equal to the number of 13 | #' elements in \code{lng} or \code{lat} and number of column equal to the 14 | #' number of variables to represent. If parameter \code{time} is set, the 15 | #' number of rows must be equal to the length of \code{lng} times the number 16 | #' of unique time steps in the data. 17 | #' @param time A vector with length equal to the number of rows in \code{chartdata} 18 | #' and containing either numbers representing time indices or dates or 19 | #' datetimes. Each unique value must appear as many times as the others. This 20 | #' parameter can be used when one wants to represent the evolution of some 21 | #' variables on a map. 22 | #' @param maxValues maximal absolute values of the variables to represent. 23 | #' It can be a vector with one value per column of \code{chartdata} or a single 24 | #' value. Using a single value enforces charts to use a unique scale for all 25 | #' variables. If it is \code{NULL}, the maximum value of \code{chartdata} is used. 26 | #' @param type Type of chart. Possible values are \code{"bar"} for bar charts, 27 | #' \code{"pie"} for pie charts, \code{"polar-area"} and \code{"polar-radius"} 28 | #' for polar area charts where the values are represented respectively by the 29 | #' area or the radius of the slices. Finally it can be equal to \code{"auto"}, 30 | #' the default. In this case, if there is only one variable to represent, the 31 | #' chart will be a single circle, else it is a barchart. 32 | #' @param fillColor Used only if data contains only one column. It is the color 33 | #' used to fill the circles. 34 | #' @param colorPalette Color palette to use when \code{chartdata} contains more than 35 | #' one column. 36 | #' @param width maximal width of the created elements. 37 | #' @param height maximal height of the created elements. 38 | #' @param opacity Opacity of the chart. 39 | #' @param showLabels Should values be displayed above chart elements. 40 | #' @param labelText character vector containing the text content of the charts. 41 | #' Used only if \code{chartdata} contains only one column. 42 | #' @param labelStyle Character string containing CSS properties to apply to the 43 | #' labels. 44 | #' @param labelMinSize Minimal height of labels in pixels. When there is not 45 | #' enough space for labels, they are hidden. 46 | #' @param labelMaxSize Maximal height of labels in pixels. 47 | #' @param transitionTime Duration in milliseconds of the transitions when a 48 | #' property of a chart is updated. 49 | #' @param popup Options that control popup generation. 50 | #' @param layerId An ID variable. It is mandatory when one wants to update some 51 | #' chart with \code{updateMinicharts}. 52 | #' @param legend If TRUE and if data has column names, then a legend is 53 | #' automatically added to the map. 54 | #' @param legendPosition Where should legend be placed? 55 | #' @param timeFormat Character string used to format dates and times when 56 | #' argument \code{time} is a \code{Date}, \code{POSIXct} or \code{POSIXlt} 57 | #' object. See \code{\link[base]{strptime}} for more information. 58 | #' @param initialTime This argument can be used to set the initial time step 59 | #' shown when the map is created. It is used only when argument \code{time} is 60 | #' set. 61 | #' @param onChange (For power users who know javascript) A character string 62 | #' containing javascript code that is executed each time a chart is updated. 63 | #' See the details section to understand why and how to use this parameter. 64 | #' @param popupOptions Change default popupOptions (ex : autoClose, maxHeight, closeButton ...) 65 | #' See \code{\link[leaflet]{popupOptions}} for more informations. 66 | #' 67 | #' @details 68 | #' Since version 0.5, the parameter \code{onChange} can be used to execute 69 | #' some arbitrary javascript code each time a chart is updated (with 70 | #' \code{updateMinicharts()} or when time step changes). A typical use case 71 | #' would be to change the color of a polygon added with 72 | #' \code{\link[leaflet]{addPolygons}} based on the data of the chart. It is even 73 | #' possible to create an invisible chart and use it to manage the color and the 74 | #' popup of a polygon. Here is a sample code that do that: 75 | #' 76 | #' \preformatted{ 77 | #' leaflet() \%>\% addTiles() \%>\% 78 | #' addPolygons(data = myPolygons, layerId = myPolygons$myIds) \%>\% 79 | #' addMinicharts( 80 | #' mydata$lon, mydata$lat, 81 | #' time = mydata$time 82 | #' fillColor = mydata$color, 83 | #' layerId = mydata$myIds, 84 | #' width = 0, height = 0, 85 | #' onChange = " 86 | #' var s = this._map.layerManager.getLayer("shape", this.layerId); 87 | #' s.bindPopup(popup); 88 | #' if (opts.fillColor) { 89 | #' d3.select(s._path) 90 | #' .transition() 91 | #' .duration(750) 92 | #' .attr("fill", opts.fillColor); 93 | #' }" 94 | #' ) 95 | #' 96 | #' } 97 | #' 98 | #' The following objects are available when executing the javascript code: 99 | #' \describe{ 100 | #' \item{this}{The current minichart object. See 101 | #' \url{https://rte-antares-rpackage.github.io/leaflet.minichart/-_L.Minichart_.html} 102 | #' for more information. 103 | #' } 104 | #' \item{opts}{The current options passed to the current minichart object.} 105 | #' \item{popup}{Popup html.} 106 | #' \item{d3}{The D3 module.} 107 | #' } 108 | #' 109 | #' Here is a toy example 110 | #' 111 | #' 112 | #' @return 113 | #' The modified leaflet map object. \code{addMinicharts} add new minicharts to 114 | #' the map. \code{updateMinicharts} updates minicharts that have already been 115 | #' added to the map. \code{removeMinicharts} removes some specific charts from 116 | #' the map and \code{clearMinicharts} removes all charts from the map and 117 | #' if necessary the legend that has been automatically created. 118 | #' 119 | #' @examples 120 | #' require(leaflet) 121 | #' mymap <- leaflet() %>% addTiles() %>% addMinicharts(0, 0, chartdata = 1:3, layerId = "c1") 122 | #' 123 | #' mymap 124 | #' mymap %>% updateMinicharts("c1", maxValues = 6) 125 | #' mymap %>% updateMinicharts("c1", type="pie") 126 | #' 127 | #' # popupOptions 128 | #' mymap <- leaflet() %>% addTiles() %>% 129 | #' addMinicharts(0, 0, chartdata = 1:3, layerId = "c1", popupOptions = list(closeButton = FALSE)) 130 | #' 131 | #' mymap 132 | #' mymap %>% updateMinicharts("c1", maxValues = 6, popupOptions = list(closeButton = TRUE)) 133 | #' 134 | #' @export 135 | #' 136 | addMinicharts <- function(map, lng, lat, chartdata = 1, time = NULL, maxValues = NULL, type = "auto", 137 | fillColor = d3.schemeCategory10[1], colorPalette = d3.schemeCategory10, 138 | width = 30, height = 30, opacity = 1, showLabels = FALSE, 139 | labelText = NULL, labelMinSize = 8, labelMaxSize = 24, 140 | labelStyle = NULL, 141 | transitionTime = 750, 142 | popup = popupArgs(), 143 | layerId = NULL, legend = TRUE, legendPosition = "topright", 144 | timeFormat = NULL, initialTime = NULL, onChange = NULL, 145 | popupOptions = NULL) { 146 | # Prepare options 147 | type <- match.arg(type, c("auto", "bar", "pie", "polar-area", "polar-radius")) 148 | if (is.null(layerId)) layerId <- sprintf("_minichart (%s,%s)", lng, lat) 149 | if (is.null(time)) time <- 1 150 | if (is.null(popup$labels)) popup$labels <- colnames(chartdata) 151 | 152 | if (showLabels) { 153 | if (!is.null(labelText)) labels <- labelText 154 | else labels <- "auto" 155 | } else { 156 | labels <- "none" 157 | } 158 | 159 | options <- .preprocessArgs( 160 | required = list(lng = lng, lat = lat, layerId = layerId, time = time), 161 | optional = list(type = type, width = width, height = height, 162 | opacity = opacity, labels = labels, 163 | labelMinSize = labelMinSize, labelMaxSize = labelMaxSize, 164 | labelStyle = labelStyle, 165 | transitionTime = transitionTime, fillColor = fillColor) 166 | ) 167 | 168 | args <- .prepareJSArgs(options, chartdata, popup, onChange, 169 | initialTime = initialTime, timeFormat = timeFormat) 170 | 171 | if (is.null(maxValues)) maxValues <- args$maxValues 172 | 173 | # Add minichart and font-awesome to the map dependencies 174 | map$dependencies <- c(map$dependencies, minichartDeps()) 175 | 176 | # control maxValues 177 | if(!is.null(maxValues)){ 178 | if(!is.null(args$chartdata)){ 179 | if(!(length(maxValues) == 1 | length(maxValues) == ncol(args$chartdata[[1]]))){ 180 | stop("'maxValues' should be a single number or have same length as 'data'") 181 | } 182 | } 183 | maxValues <- unname(maxValues) 184 | maxValues[maxValues == 0 | is.na(maxValues) | is.infinite(maxValues)] <- 1 185 | } 186 | 187 | map <- invokeMethod(map, data = leaflet::getMapData(map), "addMinicharts", 188 | args$options, args$chartdata, maxValues, colorPalette, 189 | args$timeLabels, args$initialTime, args$popupArgs, args$onChange, 190 | popupOptions) 191 | 192 | if (legend && length(args$legendLab) > 0 && args$ncol > 1) { 193 | legendCol <- colorPalette[(seq_len(args$ncols)-1) %% args$ncols + 1] 194 | map <- addLegend(map, labels = args$legendLab, colors = legendCol, opacity = 1, 195 | layerId = "minichartsLegend", position = legendPosition) 196 | } 197 | 198 | map %>% expandLimits(lat, lng) 199 | } 200 | 201 | #' @export 202 | #' @rdname addMinicharts 203 | updateMinicharts <- function(map, layerId, chartdata = NULL, time = NULL, maxValues = NULL, type = NULL, 204 | fillColor = NULL, colorPalette = d3.schemeCategory10, 205 | width = NULL, height = NULL, opacity = NULL, showLabels = NULL, 206 | labelText = NULL, labelMinSize = NULL, 207 | labelMaxSize = NULL, labelStyle = NULL, 208 | transitionTime = NULL, popup = NULL, 209 | legend = TRUE, legendPosition = NULL, 210 | timeFormat = NULL, initialTime = NULL, onChange = NULL, 211 | popupOptions = NULL) { 212 | 213 | if (!is.null(type)) { 214 | type <- match.arg(type, c("auto", "bar", "pie", "polar-area", "polar-radius")) 215 | } 216 | if (is.null(time)) time <- 1 217 | if (!is.null(chartdata) & !is.null(popup) & is.null(popup$labels)) popup$labels <- colnames(chartdata) 218 | 219 | if (is.null(showLabels)) { 220 | labels <- NULL 221 | } else { 222 | if (showLabels) { 223 | if (!is.null(labelText)) labels <- labelText 224 | else labels <- "auto" 225 | } else { 226 | labels <- "none" 227 | } 228 | } 229 | 230 | options <- .preprocessArgs( 231 | required = list(layerId = layerId, time = time), 232 | optional = list(type = type, width = width, height = height, 233 | opacity = opacity, labels = labels, 234 | labelMinSize = labelMinSize, labelMaxSize = labelMaxSize, 235 | labelStyle = labelStyle, 236 | labelText = labelText, transitionTime = transitionTime, 237 | fillColor = fillColor) 238 | ) 239 | 240 | args <- .prepareJSArgs(options, chartdata, popup, onChange, 241 | initialTime = initialTime, timeFormat = timeFormat) 242 | 243 | # Update time slider only if data is updated 244 | if(is.null(chartdata)) { 245 | args$timeLabels <- NULL 246 | } 247 | 248 | # Update legend if required 249 | if (!is.null(args$chartdata)) { 250 | if (legend && length(args$legendLab) > 0 && args$ncols > 1) { 251 | legendCol <- colorPalette[(seq_len(args$ncols)-1) %% args$ncols + 1] 252 | map <- addLegend(map, labels = args$legendLab, colors = legendCol, opacity = 1, 253 | layerId = "minichartsLegend", position = legendPosition) 254 | } else { 255 | map <- leaflet::removeControl(map, "minichartsLegend") 256 | } 257 | } 258 | 259 | # control maxValues 260 | if(!is.null(maxValues)){ 261 | if(!is.null(args$chartdata)){ 262 | if(!(length(maxValues) == 1 | length(maxValues) == ncol(args$chartdata[[1]]))){ 263 | stop("'maxValues' should be a single number or have same length as 'data'") 264 | } 265 | } 266 | maxValues <- unname(maxValues) 267 | maxValues[maxValues == 0 | is.na(maxValues) | is.infinite(maxValues)] <- 1 268 | } 269 | 270 | map %>% 271 | invokeMethod(leaflet::getMapData(map), "updateMinicharts", 272 | args$options, args$chartdata, maxValues, colorPalette, 273 | args$timeLabels, args$initialTime, args$popupArgs, 274 | args$legendLab, args$onChange, popupOptions) 275 | } 276 | 277 | #' @rdname addMinicharts 278 | #' @export 279 | removeMinicharts <- function(map, layerId) { 280 | invokeMethod(map, leaflet::getMapData(map), "removeMinicharts", layerId) 281 | } 282 | 283 | #' @rdname addMinicharts 284 | #' @export 285 | clearMinicharts <- function(map) { 286 | invokeMethod(map, leaflet::getMapData(map), "clearMinicharts") %>% 287 | leaflet::removeControl("minichartsLegend") 288 | } 289 | -------------------------------------------------------------------------------- /inst/font-awesome-4.7.0/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} 5 | --------------------------------------------------------------------------------