From fdb8bf78aaab8f784107bf0f04577d9ab4305eb3 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 02:23:51 +0200 Subject: [PATCH 01/26] workiing pie-chart --- DESCRIPTION | 5 +- NAMESPACE | 1 + R/clusterCharts.R | 117 +++++++++ inst/examples/clusterCharts_app.R | 82 ++++++ .../MarkerCluster.Default.css | 60 +++++ .../lfx-clustercharts/MarkerCluster.css | 14 + .../lfx-clustercharts/d3.v3.min.js | 5 + .../leaflet.markercluster.js | 3 + .../lfx-clustercharts-bindings.js | 241 ++++++++++++++++++ .../lfx-clustercharts/lfx-clustercharts.css | 52 ++++ man/addClusterCharts.Rd | 85 ++++++ 11 files changed, 664 insertions(+), 1 deletion(-) create mode 100644 R/clusterCharts.R create mode 100644 inst/examples/clusterCharts_app.R create mode 100644 inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css create mode 100644 inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css create mode 100644 inst/htmlwidgets/lfx-clustercharts/d3.v3.min.js create mode 100644 inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js create mode 100644 inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js create mode 100644 inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css create mode 100644 man/addClusterCharts.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 3e420059..789e5076 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,7 +28,10 @@ Suggests: fontawesome, htmlwidgets, covr, - curl + curl, + knitr, + rmarkdown URL: https://trafficonese.github.io/leaflet.extras2/, https://github.com/trafficonese/leaflet.extras2 BugReports: https://github.com/trafficonese/leaflet.extras2/issues RoxygenNote: 7.3.1 +VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 9e66f002..9452ef74 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ S3method("[",leaflet_mapkey_icon_set) export(addAntpath) export(addArrowhead) +export(addClusterCharts) export(addContextmenu) export(addEasyprint) export(addGIBS) diff --git a/R/clusterCharts.R b/R/clusterCharts.R new file mode 100644 index 00000000..4779c029 --- /dev/null +++ b/R/clusterCharts.R @@ -0,0 +1,117 @@ +# JS +# https://api.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v0.4.0/leaflet.markercluster.js +# CSS +# https://api.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v0.4.0/MarkerCluster.css +clusterchartsDependencies <- function() { + list( + htmltools::htmlDependency( + "lfx-clustercharts", version = "1.0.0", + src = system.file("htmlwidgets/lfx-clustercharts", package = "leaflet.extras2"), + stylesheet = c("MarkerCluster.css", + "MarkerCluster.Default.css", + "lfx-clustercharts.css"), + script = c( + "leaflet.markercluster.js", + "d3.v3.min.js", + "lfx-clustercharts-bindings.js" + ) + ) + ) +} + + +#' addClusterCharts +#' @description Adds a Great Circle to the map. +#' @param lat_center,lng_center lat/lng for the center +#' @param radius in meters +#' @param rmax The maxClusterRadius +#' @param categoryField in meters +#' @param popupFields in meters +#' @inheritParams leaflet::addCircleMarkers +#' @export +addClusterCharts <- function( + map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, + rmax = 30, size = c(18, 18), + popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, + clusterOptions = NULL, clusterId = NULL, + categoryField, categoryMap, popupFields = NULL, popupLabels = NULL, + markerOptions = NULL, legendOptions = list(title = "", + position = "topright"), + data = getMapData(map)) { + + ## Check labelOptions, popupFields, popupLabels, clusterOptions ############ + if (missing(labelOptions)) + labelOptions <- labelOptions() + if (!is.null(popupFields) && is.null(popupLabels)) { + popupLabels <- popupFields + } + if (!is.null(clusterOptions)) { + clusterOptions$maxClusterRadius = NULL + clusterOptions$iconCreateFunction = NULL + } + + ## CSS string ############# + # browser() + css <- paste(apply(categoryMap, 1, generate_css), collapse = "\n") + if (length(size) == 1) size <- rep(size, 2) + css <- paste0(css, "\n.marker {width: ",size[1],"px;height: ",size[1],"px;}") + csssrc <- list( + htmltools::htmlDependency( + "lfx-clustercharts-css", version = "1.0.0", + head = as.character(tags$style(css)), + src = system.file("htmlwidgets/lfx-clustercharts", package = "leaflet.extras2") + ) + ) + categoryMap <- setNames(as.list(categoryMap$label), seq.int(as.list(categoryMap$label))) + + ## Add Deps ############ + map$dependencies <- c(map$dependencies, csssrc, clusterchartsDependencies()) + + ## Make Geojson ########### + geojson <- geojsonsf::sf_geojson(data) + + ## Derive Points and Invoke Method ################## + points <- derivePoints(data, lng, lat, missing(lng), missing(lat), + "addClusterCharts") + leaflet::invokeMethod( + map, NULL, "addClusterCharts", geojson, layerId, group, rmax, size, + popup, popupOptions, safeLabel(label, data), labelOptions, + clusterOptions, clusterId, + categoryField, categoryMap, popupFields, popupLabels, + markerOptions, legendOptions + ) %>% + leaflet::expandLimits(points$lat, points$lng) +} + + +generate_css <- function(row) { + label <- row["label"] + color <- row["color"] + stroke <- row["stroke"] + icons <- row['icons'] + if (is.null(color)) color <- stroke + if (is.null(stroke)) stroke <- color + + # Replace spaces with dots in the class name + class_name <- paste0("category-",gsub(" ", ".", label)) + + css <- paste0( + ".", class_name, " {\n", + " fill: ", color, ";\n", + " stroke: ", stroke, ";\n", + " background: ", color, ";\n", + " border-color: ", stroke, ";\n", + "}\n" + ) + if (!is.null(icons)) { + css <- paste0(css, + ".icon-", gsub(" ", ".", label), " {\n", + " background-image: url('", icons, "') !important;\n", + " background-repeat: no-repeat !important;\n", + " background-position: 0px 1px !important;\n", + "}" + ) + } + css +} + diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R new file mode 100644 index 00000000..f1e6336d --- /dev/null +++ b/inst/examples/clusterCharts_app.R @@ -0,0 +1,82 @@ +library(shiny) +library(sf) +library(geojsonsf) +library(leaflet) +library(leaflet.extras2) + +data <- sf::st_as_sf(breweries91) +data$category <- sample(factor(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), ordered = TRUE), size = nrow(data), replace = TRUE) +data$label <- paste0(data$brewery, "
", data$address) +data$id <- paste0("ID", seq.int(nrow(data))) +data$popup <- paste0("
", data$brewery, "
", data$address, "
") +# data <- geojson_sf("https://gist.githubusercontent.com/gisminister/10001728/raw/97156c7676f85a1f2689ce0adceec3a759baa359/traffic_accidents.geojson") +# data <- "https://gist.githubusercontent.com/gisminister/10001728/raw/97156c7676f85a1f2689ce0adceec3a759baa359/traffic_accidents.geojson" + +ui <- fluidPage( + # tags$head(tags$style(css)), + leafletOutput("map", height = 500), + splitLayout(cellWidths = paste0(rep(20,4), "%"), + div(verbatimTextOutput("click")), + div(verbatimTextOutput("mouseover")), + div(verbatimTextOutput("mouseout")), + div(verbatimTextOutput("dragend")) + ) +) + +server <- function(input, output, session) { + output$map <- renderLeaflet({ + leaflet() %>% addMapPane("clusterpane", 420) %>% + addProviderTiles("CartoDB") %>% + leaflet::addLayersControl(overlayGroups = "clustermarkers") %>% + # addCircleMarkers(data = data, clusterOptions = markerClusterOptions()) %>% + # addCircleMarkers(data = data) %>% + addClusterCharts(data = data + , rmax = 50 + , size = 50 + , categoryField = "category" + , categoryMap = + data.frame(label = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + # color = c("#F88", "#FA0", "#FF3", "#BFB"), + # stroke = c("#800", "#B60", "#D80", "#070") + color = c("lightblue", "orange", "lightyellow", "lightgreen"), + icons = c("icons/Icon 1.svg", "icons/Icon 10.svg", "icons/Icon 20.svg", "icons/Icon 25.svg"), + # color = c("blue", "darkorange", "yellow", "green"), + stroke = "black" + ) + , group = "clustermarkers" + # group = "zipcode", + , layerId = "id" + , clusterId = "id" + , popupFields = c("brewery","address","zipcode", "category") + , popupLabels = c("Brauerei","Addresse","PLZ", "Art") + # , popup = "popup" + , label = "label" + ## Options ############# + , markerOptions = markerOptions(interactive = TRUE, + draggable = TRUE, + keyboard = TRUE, + title = "Some Marker Title", + alt = "The alt info", + zIndexOffset = 100, + opacity = 1, + riseOnHover = TRUE, + riseOffset = 400) + , legendOptions = list(position = "bottomright", title = "Unfälle im Jahr 2003") + , clusterOptions = markerClusterOptions(showCoverageOnHover = TRUE, + zoomToBoundsOnClick = TRUE, + spiderfyOnMaxZoom = TRUE, + removeOutsideVisibleBounds = TRUE, + spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5), + freezeAtZoom = TRUE, + spiderfyDistanceMultiplier = 2, + clusterPane = "clusterpane") + , labelOptions = labelOptions(opacity = 0.8, textsize = "14px") + , popupOptions = popupOptions(maxWidth = 900, minWidth = 200, keepInView = TRUE) + ) + }) + output$click <- renderPrint({input$map_marker_click}) + output$mouseover <- renderPrint({input$map_marker_mouseover}) + output$mouseout <- renderPrint({input$map_marker_mouseout}) + output$dragend <- renderPrint({input$map_marker_dragend}) +} +shinyApp(ui, server) diff --git a/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css b/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css new file mode 100644 index 00000000..6eb5a21b --- /dev/null +++ b/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css @@ -0,0 +1,60 @@ +.marker-cluster-small { + background-color: rgba(181, 226, 140, 0.6); +} +.marker-cluster-small div { + background-color: rgba(110, 204, 57, 0.6); +} + +.marker-cluster-medium { + background-color: rgba(241, 211, 87, 0.6); +} +.marker-cluster-medium div { + background-color: rgba(240, 194, 12, 0.6); +} + +.marker-cluster-large { + background-color: rgba(253, 156, 115, 0.6); +} +.marker-cluster-large div { + background-color: rgba(241, 128, 23, 0.6); +} + +/* IE 6-8 fallback colors */ + .leaflet-oldie .marker-cluster-small { + background-color: rgb(181, 226, 140); + } +.leaflet-oldie .marker-cluster-small div { + background-color: rgb(110, 204, 57); +} + +.leaflet-oldie .marker-cluster-medium { + background-color: rgb(241, 211, 87); +} +.leaflet-oldie .marker-cluster-medium div { + background-color: rgb(240, 194, 12); +} + +.leaflet-oldie .marker-cluster-large { + background-color: rgb(253, 156, 115); +} +.leaflet-oldie .marker-cluster-large div { + background-color: rgb(241, 128, 23); +} + +.marker-cluster { + background-clip: padding-box; + border-radius: 20px; +} +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; +} +.marker-cluster span { + line-height: 30px; +} diff --git a/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css b/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css new file mode 100644 index 00000000..c60d71b7 --- /dev/null +++ b/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css @@ -0,0 +1,14 @@ +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; + -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; + -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; + transition: transform 0.3s ease-out, opacity 0.3s ease-in; +} + +.leaflet-cluster-spider-leg { + /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ + -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; + -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; + -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; + transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; +} diff --git a/inst/htmlwidgets/lfx-clustercharts/d3.v3.min.js b/inst/htmlwidgets/lfx-clustercharts/d3.v3.min.js new file mode 100644 index 00000000..fe7192e5 --- /dev/null +++ b/inst/htmlwidgets/lfx-clustercharts/d3.v3.min.js @@ -0,0 +1,5 @@ +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++ie;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t0&&(n=n.slice(0,a));var c=To.get(n);return c&&(n=c,l=B),a?t?i:r:t?b:u}function $(n,t){return function(e){var r=ao.event;ao.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ao.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Do,i="click"+r,u=ao.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ro&&(Ro="onselectstart"in e?!1:x(e.style,"userSelect")),Ro){var o=n(e).style,a=o[Ro];o[Ro]="none"}return function(n){if(u.on(r,null),Ro&&(o[Ro]=a),n){var t=function(){u.on(i,null)};u.on(i,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var i=r.createSVGPoint();if(0>Po){var u=t(n);if(u.scrollX||u.scrollY){r=ao.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Po=!(o.f||o.e),r.remove()}}return Po?(i.x=e.pageX,i.y=e.pageY):(i.x=e.clientX,i.y=e.clientY),i=i.matrixTransform(n.getScreenCTM().inverse()),[i.x,i.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ao.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Fo:Math.acos(n)}function tn(n){return n>1?Io:-1>n?-Io:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function on(n){return(n=Math.sin(n/2))*n}function an(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(o-u)*n/60:180>n?o:240>n?u+(o-u)*(240-n)/60:u}function i(n){return Math.round(255*r(n))}var u,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,u=2*e-o,new mn(i(n+120),i(n),i(n-120))}function fn(n,t,e){return this instanceof fn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof fn?new fn(n.h,n.c,n.l):n instanceof hn?gn(n.l,n.a,n.b):gn((n=Sn((n=ao.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new fn(n,t,e)}function sn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Yo)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof fn?sn(n.h,n.c,n.l):Sn((n=mn(n)).r,n.g,n.b):new hn(n,t,e)}function pn(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=vn(i)*na,r=vn(r)*ta,u=vn(u)*ea,new mn(yn(3.2404542*i-1.5371385*r-.4985314*u),yn(-.969266*i+1.8760108*r+.041556*u),yn(.0556434*i-.2040259*r+1.0572252*u))}function gn(n,t,e){return n>0?new fn(Math.atan2(e,t)*Zo,Math.sqrt(t*t+e*e),n):new fn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function yn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mn(n,t,e){return this instanceof mn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mn?new mn(n.r,n.g,n.b):_n(""+n,mn,cn):new mn(n,t,e)}function Mn(n){return new mn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,i,u,o=0,a=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Nn(i[0]),Nn(i[1]),Nn(i[2]))}return(u=ua.get(n))?t(u.r,u.g,u.b):(null==n||"#"!==n.charAt(0)||isNaN(u=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&u)>>4,o=o>>4|o,a=240&u,a=a>>4|a,l=15&u,l=l<<4|l):7===n.length&&(o=(16711680&u)>>16,a=(65280&u)>>8,l=255&u)),t(o,a,l))}function wn(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-u,l=(o+u)/2;return a?(i=.5>l?a/(o+u):a/(2-o-u),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=NaN,i=l>0&&1>l?0:r),new ln(r,i,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/na),i=dn((.2126729*n+.7151522*t+.072175*e)/ta),u=dn((.0193339*n+.119192*t+.9503041*e)/ea);return hn(116*i-16,500*(r-i),200*(i-u))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function i(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(u,l)}catch(r){return void o.error.call(u,r)}o.load.call(u,n)}else o.error.call(u,l)}var u={},o=ao.dispatch("beforesend","progress","load","error"),a={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=i:l.onreadystatechange=function(){l.readyState>3&&i()},l.onprogress=function(n){var t=ao.event;ao.event=n;try{o.progress.call(u,l)}finally{ao.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(c=n,u):c},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(co(arguments)))}}),u.send=function(e,r,i){if(2===arguments.length&&"function"==typeof r&&(i=r,r=null),l.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),l.setRequestHeader)for(var f in a)l.setRequestHeader(f,a[f]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),o.beforesend.call(u,l),l.send(null==r?null:r),u},u.abort=function(){return l.abort(),u},ao.rebind(u,o,"on"),null==r?u:u.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var i=e+t,u={c:n,t:i,n:null};return aa?aa.n=u:oa=u,aa=u,la||(ca=clearTimeout(ca),la=1,fa(Tn)),u}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(ca),ca=setTimeout(Tn,t)),la=0):(la=1,fa(Tn))}function Rn(){for(var n=Date.now(),t=oa;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=oa,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,i=n.currency,u=r&&e?function(n,t){for(var i=n.length,u=[],o=0,a=r[0],l=0;i>0&&a>0&&(l+a+1>t&&(a=Math.max(1,t-l)),u.push(n.substring(i-=a,i+a)),!((l+=a+1)>t));)a=r[o=(o+1)%r.length];return u.reverse().join(e)}:m;return function(n){var e=ha.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",l=e[4]||"",c=e[5],f=+e[6],s=e[7],h=e[8],p=e[9],g=1,v="",d="",y=!1,m=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===o)&&(c=r="0",o="="),p){case"n":s=!0,p="g";break;case"%":g=100,d="%",p="f";break;case"p":g=100,d="%",p="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+p.toLowerCase());case"c":m=!1;case"d":y=!0,h=0;break;case"s":g=-1,p="r"}"$"===l&&(v=i[0],d=i[1]),"r"!=p||h||(p="g"),null!=h&&("g"==p?h=Math.max(1,Math.min(21,h)):"e"!=p&&"f"!=p||(h=Math.max(0,Math.min(20,h)))),p=pa.get(p)||Fn;var M=c&&s;return function(n){var e=d;if(y&&n%1)return"";var i=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>g){var l=ao.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=g;n=p(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=m?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&s&&(x=u(x,1/0));var S=v.length+x.length+b.length+(M?0:i.length),k=f>S?new Array(S=f-S+1).join(r):"";return M&&(x=u(k+x,k.length?f-b.length:1/0)),i+=v,n=x+b,("<"===o?i+n+k:">"===o?k+i+n:"^"===o?k.substring(0,S>>=1)+i+n+k.substring(S):i+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new va(e-1)),1),e}function u(n,e){return t(n=new va(+n),e),n}function o(n,r,u){var o=i(n),a=[];if(u>1)for(;r>o;)e(o)%u||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{va=Hn;var r=new Hn;return r._=n,o(r,t,e)}finally{va=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=o;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(i),l.offset=In(u),l.range=a,n}function In(n){return function(t,e){try{va=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{va=Date}}}function Yn(n){function t(n){function t(t){for(var e,i,u,o=[],a=-1,l=0;++aa;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(o=t.charAt(a++),u=C[o in ya?t.charAt(a++):o],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function f(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var s=n.dateTime,h=n.date,p=n.time,g=n.periods,v=n.days,d=n.shortDays,y=n.months,m=n.shortMonths;t.utc=function(n){function e(n){try{va=Hn;var t=new va;return t._=n,r(t)}finally{va=Date}}var r=t(n);return e.parse=function(n){try{va=Hn;var t=r.parse(n);return t&&t._}finally{va=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=ao.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(y),k=Xn(y),N=Vn(m),E=Xn(m);g.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return m[n.getMonth()]},B:function(n){return y[n.getMonth()]},c:t(s),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ga.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return g[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ga.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ga.mondayOfYear(n),t,2)},x:t(h),X:t(p),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:at,"%":function(){return"%"}},C={a:r,A:i,b:u,B:o,c:a,d:tt,e:tt,H:rt,I:rt,j:et,L:ot,m:nt,M:it,p:f,S:ut,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?new Array(e-u+1).join(t)+i:i)}function Vn(n){return new RegExp("^(?:"+n.map(ao.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function it(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function ut(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ot(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function at(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=xo(t)/60|0,i=xo(t)%60;return e+Zn(r,"0",2)+Zn(i,"0",2)}function lt(n,t,e){Ma.lastIndex=0;var r=Ma.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,l=Math.cos(t),c=Math.sin(t),f=u*c,s=i*l+f*Math.cos(a),h=f*o*Math.sin(a);ka.add(Math.atan2(h,s)),r=n,i=l,u=c}var t,e,r,i,u;Na.point=function(o,a){Na.point=n,r=(t=o)*Yo,i=Math.cos(a=(e=a)*Yo/2+Fo/4),u=Math.sin(a)},Na.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function yt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function mt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return xo(n[0]-t[0])a;++a)i.point((e=n[a])[0],e[1]);return void i.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,u.push(l),o.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,u.push(l),o.push(c)}}),o.sort(t),qt(u),qt(o),u.length){for(var a=0,l=e,c=o.length;c>a;++a)o[a].e=l=!l;for(var f,s,h=u[0];;){for(var p=h,g=!0;p.v;)if((p=p.n)===h)return;f=p.z,i.lineStart();do{if(p.v=p.o.v=!0,p.e){if(g)for(var a=0,c=f.length;c>a;++a)i.point((s=f[a])[0],s[1]);else r(p.x,p.n.x,1,i);p=p.n}else{if(g){f=p.p.z;for(var a=f.length-1;a>=0;--a)i.point((s=f[a])[0],s[1])}else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,f=p.z,g=!g}while(!p.v);i.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,i=n[0];++r0){for(b||(u.polygonStart(),b=!0),u.lineStart();++o1&&2&t&&e.push(e.pop().concat(e.shift())),p.push(e.filter(Dt))}var p,g,v,d=t(u),y=i.invert(r[0],r[1]),m={point:o,lineStart:l,lineEnd:c,polygonStart:function(){m.point=f,m.lineStart=s,m.lineEnd=h,p=[],g=[]},polygonEnd:function(){m.point=o,m.lineStart=l,m.lineEnd=c,p=ao.merge(p);var n=Ot(y,g);p.length?(b||(u.polygonStart(),b=!0),Lt(p,Ut,n,e,u)):n&&(b||(u.polygonStart(),b=!0),u.lineStart(),e(null,null,1,u),u.lineEnd()),b&&(u.polygonEnd(),b=!1),p=g=null},sphere:function(){u.polygonStart(),u.lineStart(),e(null,null,1,u),u.lineEnd(),u.polygonEnd()}},M=Pt(),x=t(M),b=!1;return m}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Io-Uo:Io-n[1])-((t=t.x)[0]<0?t[1]-Io-Uo:Io-t[1])}function jt(n){var t,e=NaN,r=NaN,i=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(u,o){var a=u>0?Fo:-Fo,l=xo(u-e);xo(l-Fo)0?Io:-Io),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(u,r),t=0):i!==a&&l>=Fo&&(xo(e-i)Uo?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*o)):(t+r)/2}function Ht(n,t,e,r){var i;if(null==n)i=e*Io,r.point(-Fo,i),r.point(0,i),r.point(Fo,i),r.point(Fo,0),r.point(Fo,-i),r.point(0,-i),r.point(-Fo,-i),r.point(-Fo,0),r.point(-Fo,i);else if(xo(n[0]-t[0])>Uo){var u=n[0]a;++a){var c=t[a],f=c.length;if(f)for(var s=c[0],h=s[0],p=s[1]/2+Fo/4,g=Math.sin(p),v=Math.cos(p),d=1;;){d===f&&(d=0),n=c[d];var y=n[0],m=n[1]/2+Fo/4,M=Math.sin(m),x=Math.cos(m),b=y-h,_=b>=0?1:-1,w=_*b,S=w>Fo,k=g*M;if(ka.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),u+=S?b+_*Ho:b,S^h>=e^y>=e){var N=mt(dt(s),dt(n));bt(N);var E=mt(i,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=y,g=M,v=x,s=n}}return(-Uo>u||Uo>u&&-Uo>ka)^1&o}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>u}function e(n){var e,u,l,c,f;return{lineStart:function(){c=l=!1,f=1},point:function(s,h){var p,g=[s,h],v=t(s,h),d=o?v?0:i(s,h):v?i(s+(0>s?Fo:-Fo),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(p=r(e,g),(wt(e,p)||wt(g,p))&&(g[0]+=Uo,g[1]+=Uo,v=t(g[0],g[1]))),v!==l)f=0,v?(n.lineStart(),p=r(g,e),n.point(p[0],p[1])):(p=r(e,g),n.point(p[0],p[1]),n.lineEnd()),e=p;else if(a&&e&&o^v){var y;d&u||!(y=r(g,e,!0))||(f=0,o?(n.lineStart(),n.point(y[0][0],y[0][1]),n.point(y[1][0],y[1][1]),n.lineEnd()):(n.point(y[1][0],y[1][1]),n.lineEnd(),n.lineStart(),n.point(y[0][0],y[0][1])))}!v||e&&wt(e,g)||n.point(g[0],g[1]),e=g,l=v,u=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return f|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),i=dt(t),o=[1,0,0],a=mt(r,i),l=yt(a,a),c=a[0],f=l-c*c;if(!f)return!e&&n;var s=u*l/f,h=-u*c/f,p=mt(o,a),g=xt(o,s),v=xt(a,h);Mt(g,v);var d=p,y=yt(g,d),m=yt(d,d),M=y*y-m*(yt(g,g)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-y-x)/m);if(Mt(b,g),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=xo(E-Fo)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(xo(b[0]-w)Fo^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-y+x)/m);return Mt(z,g),[b,_t(z)]}}}function i(t,e){var r=o?n:Fo-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}var u=Math.cos(n),o=u>0,a=xo(u)>Uo,l=ve(n,6*Yo);return Rt(t,e,l,o?[0,-n]:[-Fo,n-Fo])}function Yt(n,t,e,r){return function(i){var u,o=i.a,a=i.b,l=o.x,c=o.y,f=a.x,s=a.y,h=0,p=1,g=f-l,v=s-c;if(u=n-l,g||!(u>0)){if(u/=g,0>g){if(h>u)return;p>u&&(p=u)}else if(g>0){if(u>p)return;u>h&&(h=u)}if(u=e-l,g||!(0>u)){if(u/=g,0>g){if(u>p)return;u>h&&(h=u)}else if(g>0){if(h>u)return;p>u&&(p=u)}if(u=t-c,v||!(u>0)){if(u/=v,0>v){if(h>u)return;p>u&&(p=u)}else if(v>0){if(u>p)return;u>h&&(h=u)}if(u=r-c,v||!(0>u)){if(u/=v,0>v){if(u>p)return;u>h&&(h=u)}else if(v>0){if(h>u)return;p>u&&(p=u)}return h>0&&(i.a={x:l+h*g,y:c+h*v}),1>p&&(i.b={x:l+p*g,y:c+p*v}),i}}}}}}function Zt(n,t,e,r){function i(r,i){return xo(r[0]-n)0?0:3:xo(r[0]-e)0?2:1:xo(r[1]-t)0?1:0:i>0?3:2}function u(n,t){return o(n.x,t.x)}function o(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function l(n){for(var t=0,e=d.length,r=n[1],i=0;e>i;++i)for(var u,o=1,a=d[i],l=a.length,c=a[0];l>o;++o)u=a[o],c[1]<=r?u[1]>r&&Q(c,u,n)>0&&++t:u[1]<=r&&Q(c,u,n)<0&&--t,c=u;return 0!==t}function c(u,a,l,c){var f=0,s=0;if(null==u||(f=i(u,l))!==(s=i(a,l))||o(u,a)<0^l>0){do c.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+l+4)%4)!==s)}else c.point(a[0],a[1])}function f(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function s(n,t){f(n,t)&&a.point(n,t)}function h(){C.point=g,d&&d.push(y=[]),S=!0,w=!1,b=_=NaN}function p(){v&&(g(m,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=s,w&&a.lineEnd()}function g(n,t){n=Math.max(-Ha,Math.min(Ha,n)),t=Math.max(-Ha,Math.min(Ha,t));var e=f(n,t);if(d&&y.push([n,t]),S)m=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,y,m,M,x,b,_,w,S,k,N=a,E=Pt(),A=Yt(n,t,e,r),C={point:s,lineStart:h,lineEnd:p,polygonStart:function(){a=E,v=[],d=[],k=!0},polygonEnd:function(){a=N,v=ao.merge(v);var t=l([n,r]),e=k&&t,i=v.length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),c(null,null,1,a),a.lineEnd()),i&&Lt(v,u,t,c,a),a.polygonEnd()),v=d=y=null}};return C}}function Vt(n){var t=0,e=Fo/3,r=ae(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*Fo/180,e=n[1]*Fo/180):[t/Fo*180,e/Fo*180]},i}function Xt(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),o-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),o=Math.sqrt(u)/i;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/i,tn((u-(n*n+e*e)*i*i)/(2*i))]},e}function $t(){function n(n,t){Ia+=i*n-r*t,r=n,i=t}var t,e,r,i;$a.point=function(u,o){$a.point=n,t=r=u,e=i=o},$a.lineEnd=function(){n(t,e)}}function Bt(n,t){Ya>n&&(Ya=n),n>Va&&(Va=n),Za>t&&(Za=t),t>Xa&&(Xa=t)}function Wt(){function n(n,t){o.push("M",n,",",t,u)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function i(){o.push("Z")}var u=Jt(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return u=Jt(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ca+=n,za+=t,++La}function Kt(){function n(n,r){var i=n-t,u=r-e,o=Math.sqrt(i*i+u*u);qa+=o*(t+n)/2,Ta+=o*(e+r)/2,Ra+=o,Gt(t=n,e=r)}var t,e;Wa.point=function(r,i){Wa.point=n,Gt(t=r,e=i)}}function Qt(){Wa.point=Gt}function ne(){function n(n,t){var e=n-r,u=t-i,o=Math.sqrt(e*e+u*u);qa+=o*(r+n)/2,Ta+=o*(i+t)/2,Ra+=o,o=i*n-r*t,Da+=o*(r+n),Pa+=o*(i+t),Ua+=3*o,Gt(r=n,i=t)}var t,e,r,i;Wa.point=function(u,o){Wa.point=n,Gt(t=r=u,e=i=o)},Wa.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,Ho)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function i(){a.point=t}function u(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=i,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function ee(n){function t(n){return(a?r:e)(n)}function e(t){return ue(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=u,t.lineStart()}function u(e,r){var u=dt([e,r]),o=n(e,r);i(M,x,m,b,_,w,M=o[0],x=o[1],m=e,b=u[0],_=u[1],w=u[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function l(){ + r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=o-t,r=K(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(o/r,1/u))-Io]},e):Ne}function ke(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return xo(i)i;i++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var i=n[0],u=e[0],o=t[0]-i,a=r[0]-u,l=n[1],c=e[1],f=t[1]-l,s=r[1]-c,h=(a*(l-c)-s*(i-u))/(s*o-a*f);return[i+h*o,l+h*f]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),ol.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,i={x:e,y:r},u=n.P,o=n.N,a=[n];je(n);for(var l=u;l.circle&&xo(e-l.circle.x)f;++f)c=a[f],l=a[f-1],nr(c.edge,l.site,c.site,i);l=a[0],c=a[s-1],c.edge=Ke(l.site,c.site,null,i),$e(l),$e(c)}function He(n){for(var t,e,r,i,u=n.x,o=n.y,a=ol._;a;)if(r=Oe(a,o)-u,r>Uo)a=a.L;else{if(i=u-Ie(a,o),!(i>Uo)){r>-Uo?(t=a.P,e=a):i>-Uo?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var l=Ue(n);if(ol.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),ol.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,f=c.x,s=c.y,h=n.x-f,p=n.y-s,g=e.site,v=g.x-f,d=g.y-s,y=2*(h*d-p*v),m=h*h+p*p,M=v*v+d*d,x={x:(d*m-p*M)/y+f,y:(h*M-v*m)/y+s};nr(e.edge,c,g,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,g,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,i=e.y,u=i-t;if(!u)return r;var o=n.P;if(!o)return-(1/0);e=o.site;var a=e.x,l=e.y,c=l-t;if(!c)return a;var f=a-r,s=1/u-1/c,h=f/c;return s?(-h+Math.sqrt(h*h-2*s*(f*f/(-2*c)-l+c/2+i-u/2)))/s+r:(r+a)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,i,u,o,a,l,c,f,s=n[0][0],h=n[1][0],p=n[0][1],g=n[1][1],v=ul,d=v.length;d--;)if(u=v[d],u&&u.prepare())for(a=u.edges,l=a.length,o=0;l>o;)f=a[o].end(),r=f.x,i=f.y,c=a[++o%l].start(),t=c.x,e=c.y,(xo(r-t)>Uo||xo(i-e)>Uo)&&(a.splice(o,0,new tr(Qe(u.site,f,xo(r-s)Uo?{x:s,y:xo(t-s)Uo?{x:xo(e-g)Uo?{x:h,y:xo(t-h)Uo?{x:xo(e-p)=-jo)){var p=l*l+c*c,g=f*f+s*s,v=(s*p-c*g)/h,d=(l*g-f*p)/h,s=d+a,y=fl.pop()||new Xe;y.arc=n,y.site=i,y.x=v+o,y.y=s+Math.sqrt(v*v+d*d),y.cy=s,n.circle=y;for(var m=null,M=ll._;M;)if(y.yd||d>=a)return;if(h>g){if(u){if(u.y>=c)return}else u={x:d,y:l};e={x:d,y:c}}else{if(u){if(u.yr||r>1)if(h>g){if(u){if(u.y>=c)return}else u={x:(l-i)/r,y:l};e={x:(c-i)/r,y:c}}else{if(u){if(u.yp){if(u){if(u.x>=a)return}else u={x:o,y:r*o+i};e={x:a,y:r*a+i}}else{if(u){if(u.xu||s>o||r>h||i>p)){if(g=n.point){var g,v=t-n.x,d=e-n.y,y=v*v+d*d;if(l>y){var m=Math.sqrt(l=y);r=t-m,i=e-m,u=t+m,o=e+m,a=g}}for(var M=n.nodes,x=.5*(f+h),b=.5*(s+p),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,f,s,x,b);break;case 1:c(n,x,s,h,b);break;case 2:c(n,f,b,x,p);break;case 3:c(n,x,b,h,p)}}}(n,r,i,u,o),a}function vr(n,t){n=ao.rgb(n),t=ao.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,o=t.g-r,a=t.b-i;return function(n){return"#"+bn(Math.round(e+u*n))+bn(Math.round(r+o*n))+bn(Math.round(i+a*n))}}function dr(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function yr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function mr(n,t){var e,r,i,u=hl.lastIndex=pl.lastIndex=0,o=-1,a=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=pl.exec(t));)(i=r.index)>u&&(i=t.slice(u,i),a[o]?a[o]+=i:a[++o]=i),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,l.push({i:o,x:yr(e,r)})),u=pl.lastIndex;return ur;++r)a[(e=l[r]).i]=e.x(n);return a.join("")})}function Mr(n,t){for(var e,r=ao.interpolators.length;--r>=0&&!(e=ao.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],i=[],u=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(Mr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;o>e;++e)i[e]=t[e];return function(n){for(e=0;a>e;++e)i[e]=r[e](n);return i}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Io)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ho*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ho/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=ao.hcl(n),t=ao.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,o=t.c-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return sn(e+u*n,r+o*n,i+a*n)+""}}function Dr(n,t){n=ao.hsl(n),t=ao.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,o=t.s-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return cn(e+u*n,r+o*n,i+a*n)+""}}function Pr(n,t){n=ao.lab(n),t=ao.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,o=t.a-r,a=t.b-i;return function(n){return pn(e+u*n,r+o*n,i+a*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),i=Fr(t,e),u=Hr(Or(e,t,-i))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:yr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:yr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var i=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:i-4,x:yr(n[0],t[0])},{i:i-2,x:yr(n[1],t[1])})}else 1===t[0]&&1===t[1]||e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=ao.transform(n),t=ao.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,i=-1,u=r.length;++i=0;)e.push(i[r])}function oi(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(u=n.children)&&(i=u.length))for(var i,u,o=-1;++oe;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function yi(n){return n.reduce(mi,0)}function mi(n,t){return n+t[1]}function Mi(n,t){return xi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function bi(n){return[ao.min(n),ao.max(n)]}function _i(n,t){return n.value-t.value}function wi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Si(n,t){n._pack_next=t,t._pack_prev=n}function ki(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ni(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),p=Math.max(n.y+n.r,p)}if((e=n.children)&&(c=e.length)){var e,r,i,u,o,a,l,c,f=1/0,s=-(1/0),h=1/0,p=-(1/0);if(e.forEach(Ei),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(i=e[1],i.x=i.r,i.y=0,t(i),c>2))for(u=e[2],zi(r,i,u),t(u),wi(r,u),r._pack_prev=u,wi(u,i),i=r._pack_next,o=3;c>o;o++){zi(r,i,u=e[o]);var g=0,v=1,d=1;for(a=i._pack_next;a!==i;a=a._pack_next,v++)if(ki(a,u)){g=1;break}if(1==g)for(l=r._pack_prev;l!==a._pack_prev&&!ki(l,u);l=l._pack_prev,d++);g?(d>v||v==d&&i.ro;o++)u=e[o],u.x-=y,u.y-=m,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(Ai)}}function Ei(n){n._pack_next=n._pack_prev=n}function Ai(n){delete n._pack_next,delete n._pack_prev}function Ci(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,o=i.length;++u=0;)t=i[u],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pi(n,t,e){return n.a.parent===t.parent?n.a:e}function Ui(n){return 1+ao.max(n,function(n){return n.y})}function ji(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Hi(n){var t,e=n.children;return e&&(t=e.length)?Hi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ii(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zi(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Vi(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Xi(n,t){var e,r=0,i=n.length-1,u=n[r],o=n[i];return u>o&&(e=r,r=i,i=e,e=u,u=o,o=e),n[r]=t.floor(u),n[i]=t.ceil(o),n}function $i(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bi(n,t,e,r){var i=[],u=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?Bi:Vi,l=r?Wr:Br;return o=i(n,t,l,e),a=i(t,n,l,Mr),u}function u(n){return o(n)}var o,a;return u.invert=function(n){return a(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Qi(n,t)},u.tickFormat=function(t,e){return nu(n,t,e)},u.nice=function(t){return Gi(n,t),i()},u.copy=function(){return Wi(n,t,e,r)},i()}function Ji(n,t){return ao.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gi(n,t){return Xi(n,$i(Ki(n,t)[2])),Xi(n,$i(Ki(n,t)[2])),n}function Ki(n,t){null==t&&(t=10);var e=Yi(n),r=e[1]-e[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Qi(n,t){return ao.range.apply(ao,Ki(n,t))}function nu(n,t,e){var r=Ki(n,t);if(e){var i=ha.exec(e);if(i.shift(),"s"===i[8]){var u=ao.formatPrefix(Math.max(xo(r[0]),xo(r[1])));return i[7]||(i[7]="."+tu(u.scale(r[2]))),i[8]="f",e=ao.format(i.join("")),function(n){return e(u.scale(n))+u.symbol}}i[7]||(i[7]="."+eu(i[8],r)),e=i.join("")}else e=",."+tu(r[2])+"f";return ao.format(e)}function tu(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function eu(n,t){var e=tu(t[2]);return n in kl?Math.abs(e-tu(Math.max(xo(t[0]),xo(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ru(n,t,e,r){function i(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function u(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(i(t))}return o.invert=function(t){return u(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(i)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(i)),o):t},o.nice=function(){var t=Xi(r.map(i),e?Math:El);return n.domain(t),r=t.map(u),o},o.ticks=function(){var n=Yi(r),o=[],a=n[0],l=n[1],c=Math.floor(i(a)),f=Math.ceil(i(l)),s=t%1?2:t;if(isFinite(f-c)){if(e){for(;f>c;c++)for(var h=1;s>h;h++)o.push(u(c)*h);o.push(u(c))}else for(o.push(u(c));c++0;h--)o.push(u(c)*h);for(c=0;o[c]l;f--);o=o.slice(c,f)}return o},o.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=ao.format(e));var r=Math.max(1,t*n/o.ticks().length);return function(n){var o=n/u(Math.round(i(n)));return t-.5>o*t&&(o*=t),r>=o?e(n):""}},o.copy=function(){return ru(n.copy(),t,e,r)},Ji(o,n)}function iu(n,t,e){function r(t){return n(i(t))}var i=uu(t),u=uu(1/t);return r.invert=function(t){return u(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Qi(e,n)},r.tickFormat=function(n,t){return nu(e,n,t)},r.nice=function(n){return r.domain(Gi(e,n))},r.exponent=function(o){return arguments.length?(i=uu(t=o),u=uu(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return iu(n.copy(),t,e)},Ji(r,n)}function uu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(e){return u[((i.get(e)||("range"===t.t?i.set(e,n.push(e)):NaN))-1)%u.length]}function r(t,e){return ao.range(n.length).map(function(n){return t+e*n})}var i,u,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new c;for(var u,o=-1,a=r.length;++oe?[NaN,NaN]:[e>0?a[e-1]:n[0],et?NaN:t/u+n,[t,t+1/u]},r.copy=function(){return lu(n,t,e)},i()}function cu(n,t){function e(e){return e>=e?t[ao.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return cu(n,t)},e}function fu(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qi(n,t)},t.tickFormat=function(t,e){return nu(n,t,e)},t.copy=function(){return fu(n)},t}function su(){return 0}function hu(n){return n.innerRadius}function pu(n){return n.outerRadius}function gu(n){return n.startAngle}function vu(n){return n.endAngle}function du(n){return n&&n.padAngle}function yu(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function mu(n,t,e,r,i){var u=n[0]-t[0],o=n[1]-t[1],a=(i?r:-r)/Math.sqrt(u*u+o*o),l=a*o,c=-a*u,f=n[0]+l,s=n[1]+c,h=t[0]+l,p=t[1]+c,g=(f+h)/2,v=(s+p)/2,d=h-f,y=p-s,m=d*d+y*y,M=e-r,x=f*p-h*s,b=(0>y?-1:1)*Math.sqrt(Math.max(0,M*M*m-x*x)),_=(x*y-d*b)/m,w=(-x*d-y*b)/m,S=(x*y+d*b)/m,k=(-x*d+y*b)/m,N=_-g,E=w-v,A=S-g,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mu(n){function t(t){function o(){c.push("M",u(n(f),a))}for(var l,c=[],f=[],s=-1,h=t.length,p=En(e),g=En(r);++s1?n.join("L"):n+"Z"}function bu(n){return n.join("L")+"Z"}function _u(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1&&i.push("H",r[0]),i.join("")}function wu(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1){a=t[1],u=n[l],l++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(u[0]-a[0])+","+(u[1]-a[1])+","+u[0]+","+u[1];for(var c=2;c9&&(i=3*t/Math.sqrt(i),o[a]=i*e,o[a+1]=i*r));for(a=-1;++a<=l;)i=(n[Math.min(l,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),u.push([i||0,o[a]*i||0]);return u}function Fu(n){return n.length<3?xu(n):n[0]+Au(n,ju(n))}function Hu(n){for(var t,e,r,i=-1,u=n.length;++i=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},ao.max=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},ao.extent=function(n,t){var e,r,i,u=-1,o=n.length;if(1===arguments.length){for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}else{for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}return[e,i]},ao.sum=function(n,t){var e,r=0,u=n.length,o=-1;if(1===arguments.length)for(;++o1?l/(f-1):void 0},ao.deviation=function(){var n=ao.variance.apply(this,arguments);return n?Math.sqrt(n):n};var Mo=u(e);ao.bisectLeft=Mo.left,ao.bisect=ao.bisectRight=Mo.right,ao.bisector=function(n){return u(1===n.length?function(t,r){return e(n(t),r)}:n)},ao.shuffle=function(n,t,e){(u=arguments.length)<3&&(e=n.length,2>u&&(t=0));for(var r,i,u=e-t;u;)i=Math.random()*u--|0,r=n[u+t],n[u+t]=n[i+t],n[i+t]=r;return n},ao.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ao.pairs=function(n){for(var t,e=0,r=n.length-1,i=n[0],u=new Array(0>r?0:r);r>e;)u[e]=[t=i,i=n[++e]];return u},ao.transpose=function(n){if(!(i=n.length))return[];for(var t=-1,e=ao.min(n,o),r=new Array(e);++t=0;)for(r=n[i],t=r.length;--t>=0;)e[--o]=r[t];return e};var xo=Math.abs;ao.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,i=[],u=a(xo(e)),o=-1;if(n*=u,t*=u,e*=u,0>e)for(;(r=n+e*++o)>t;)i.push(r/u);else for(;(r=n+e*++o)=u.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,p=-1,g=o.length,v=u[a++],d=new c;++p=u.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,i={},u=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ao.map,e,0),0)},i.key=function(n){return u.push(n),i},i.sortKeys=function(n){return o[u.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ao.set=function(n){var t=new y;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(y,{has:h,add:function(n){return this._[f(n+="")]=!0,n},remove:p,values:g,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,s(t))}}),ao.behavior={},ao.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ao.event=null,ao.requote=function(n){return n.replace(So,"\\$&")};var So=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ko={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},No=function(n,t){return t.querySelector(n)},Eo=function(n,t){return t.querySelectorAll(n)},Ao=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ao=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(No=function(n,t){return Sizzle(n,t)[0]||null},Eo=Sizzle,Ao=Sizzle.matchesSelector),ao.selection=function(){return ao.select(fo.documentElement)};var Co=ao.selection.prototype=[];Co.select=function(n){var t,e,r,i,u=[];n=A(n);for(var o=-1,a=this.length;++o=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Lo.hasOwnProperty(e)?{space:Lo[e],local:n}:n}},Co.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ao.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Co.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,i=-1;if(t=e.classList){for(;++ii){if("string"!=typeof n){2>i&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>i){var u=this.node();return t(u).getComputedStyle(u,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Co.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Co.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Co.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Co.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Co.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Co.remove=function(){return this.each(F)},Co.data=function(n,t){function e(n,e){var r,i,u,o=n.length,s=e.length,h=Math.min(o,s),p=new Array(s),g=new Array(s),v=new Array(o);if(t){var d,y=new c,m=new Array(o);for(r=-1;++rr;++r)g[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}g.update=p,g.parentNode=p.parentNode=v.parentNode=n.parentNode,a.push(g),l.push(p),f.push(v)}var r,i,u=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++uu;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return E(i)},Co.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Co.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Co.size=function(){var n=0;return Y(this,function(){++n}),n};var qo=[];ao.selection.enter=Z,ao.selection.enter.prototype=qo,qo.append=Co.append,qo.empty=Co.empty,qo.node=Co.node,qo.call=Co.call,qo.size=Co.size,qo.select=function(n){for(var t,e,r,i,u,o=[],a=-1,l=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var To=ao.map({mouseenter:"mouseover",mouseleave:"mouseout"});fo&&To.forEach(function(n){"on"+n in fo&&To.remove(n)});var Ro,Do=0;ao.mouse=function(n){return J(n,k())};var Po=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ao.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,i=0,u=t.length;u>i;++i)if((r=t[i]).identifier===e)return J(n,r)},ao.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",o)}function e(n,t,e,u,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],g|=n|e,M=r,p({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(y.on(u+d,null).on(o+d,null),m(g),p({type:"dragend"}))}var c,f=this,s=ao.event.target.correspondingElement||ao.event.target,h=f.parentNode,p=r.of(f,arguments),g=0,v=n(),d=".drag"+(null==v?"":"-"+v),y=ao.select(e(s)).on(u+d,a).on(o+d,l),m=W(s),M=t(h,v);i?(c=i.apply(f,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],p({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),i=null,u=e(b,ao.mouse,t,"mousemove","mouseup"),o=e(G,ao.touch,m,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},ao.rebind(n,r,"on")},ao.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?co(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Uo=1e-6,jo=Uo*Uo,Fo=Math.PI,Ho=2*Fo,Oo=Ho-Uo,Io=Fo/2,Yo=Fo/180,Zo=180/Fo,Vo=Math.SQRT2,Xo=2,$o=4;ao.interpolateZoom=function(n,t){var e,r,i=n[0],u=n[1],o=n[2],a=t[0],l=t[1],c=t[2],f=a-i,s=l-u,h=f*f+s*s;if(jo>h)r=Math.log(c/o)/Vo,e=function(n){return[i+n*f,u+n*s,o*Math.exp(Vo*n*r)]};else{var p=Math.sqrt(h),g=(c*c-o*o+$o*h)/(2*o*Xo*p),v=(c*c-o*o-$o*h)/(2*c*Xo*p),d=Math.log(Math.sqrt(g*g+1)-g),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-d)/Vo,e=function(n){var t=n*r,e=rn(d),a=o/(Xo*p)*(e*un(Vo*t+d)-en(d));return[i+a*f,u+a*s,o*e/rn(Vo*t+d)]}}return e.duration=1e3*r,e},ao.behavior.zoom=function(){function n(n){n.on(L,s).on(Wo+".zoom",p).on("dblclick.zoom",g).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function i(n){k.k=Math.max(A[0],Math.min(A[1],n))}function u(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},i(Math.pow(2,o)),u(d=e,r),t=ao.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function f(n){--z||(n({type:"zoomend"}),d=null)}function s(){function n(){a=1,u(ao.mouse(i),h),c(o)}function r(){s.on(q,null).on(T,null),p(a),f(o)}var i=this,o=D.of(i,arguments),a=0,s=ao.select(t(i)).on(q,n).on(T,r),h=e(ao.mouse(i)),p=W(i);Il.call(i),l(o)}function h(){function n(){var n=ao.touches(g);return p=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ao.event.target;ao.select(t).on(x,r).on(b,a),_.push(t);for(var e=ao.event.changedTouches,i=0,u=e.length;u>i;++i)d[e[i].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var f=l[0];o(g,f,d[f.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var f=l[0],s=l[1],h=f[0]-s[0],p=f[1]-s[1];y=h*h+p*p}}function r(){var n,t,e,r,o=ao.touches(g);Il.call(g);for(var a=0,l=o.length;l>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var f=(f=e[0]-n[0])*f+(f=e[1]-n[1])*f,s=y&&Math.sqrt(f/y);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],i(s*p)}M=null,u(n,t),c(v)}function a(){if(ao.event.touches.length){for(var t=ao.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var i in d)return void n()}ao.selectAll(_).on(m,null),w.on(L,s).on(R,h),N(),f(v)}var p,g=this,v=D.of(g,arguments),d={},y=0,m=".zoom-"+ao.event.changedTouches[0].identifier,x="touchmove"+m,b="touchend"+m,_=[],w=ao.select(g),N=W(g);t(),l(v),w.on(L,null).on(R,t)}function p(){var n=D.of(this,arguments);m?clearTimeout(m):(Il.call(this),v=e(d=y||ao.mouse(this)),l(n)),m=setTimeout(function(){m=null,f(n)},50),S(),i(Math.pow(2,.002*Bo())*k.k),u(d,v),c(n)}function g(){var n=ao.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ao.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,y,m,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Jo,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Wo||(Wo="onwheel"in fo?(Bo=function(){return-ao.event.deltaY*(ao.event.deltaMode?120:1)},"wheel"):"onmousewheel"in fo?(Bo=function(){return ao.event.wheelDelta},"mousewheel"):(Bo=function(){return-ao.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?ao.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],i=d?d[0]:e/2,u=d?d[1]:r/2,o=ao.interpolateZoom([(i-k.x)/k.k,(u-k.y)/k.k,e/k.k],[(i-t.x)/t.k,(u-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:i-r[0]*a,y:u-r[1]*a,k:a},c(n)}}).each("interrupt.zoom",function(){f(n)}).each("end.zoom",function(){f(n)}):(this.__chart__=k,l(n),c(n),f(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},i(+t),a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Jo:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(y=t&&[+t[0],+t[1]],n):y},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ao.rebind(n,D,"on")};var Bo,Wo,Jo=[0,1/0];ao.color=an,an.prototype.toString=function(){return this.rgb()+""},ao.hsl=ln;var Go=ln.prototype=new an;Go.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Go.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Go.rgb=function(){return cn(this.h,this.s,this.l)},ao.hcl=fn;var Ko=fn.prototype=new an;Ko.brighter=function(n){return new fn(this.h,this.c,Math.min(100,this.l+Qo*(arguments.length?n:1)))},Ko.darker=function(n){return new fn(this.h,this.c,Math.max(0,this.l-Qo*(arguments.length?n:1)))},Ko.rgb=function(){return sn(this.h,this.c,this.l).rgb()},ao.lab=hn;var Qo=18,na=.95047,ta=1,ea=1.08883,ra=hn.prototype=new an;ra.brighter=function(n){return new hn(Math.min(100,this.l+Qo*(arguments.length?n:1)),this.a,this.b)},ra.darker=function(n){return new hn(Math.max(0,this.l-Qo*(arguments.length?n:1)),this.a,this.b)},ra.rgb=function(){return pn(this.l,this.a,this.b)},ao.rgb=mn;var ia=mn.prototype=new an;ia.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),new mn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mn(i,i,i)},ia.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mn(n*this.r,n*this.g,n*this.b)},ia.hsl=function(){return wn(this.r,this.g,this.b)},ia.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ua=ao.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ua.forEach(function(n,t){ua.set(n,Mn(t))}),ao.functor=En,ao.xhr=An(m),ao.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var o=Cn(n,t,null==e?r:i(e),u);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:i(n)):e},o}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function u(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return o;if(i)return i=!1,u;var t=f;if(34===n.charCodeAt(t)){for(var e=t;e++f;){var r=n.charCodeAt(f++),a=1;if(10===r)i=!0;else if(13===r)i=!0,10===n.charCodeAt(f)&&(++f,++a);else if(r!==l)continue;return n.slice(t,f-a)}return n.slice(t)}for(var r,i,u={},o={},a=[],c=n.length,f=0,s=0;(r=e())!==o;){for(var h=[];r!==u&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,s++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new y,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(u).join("\n")},e},ao.csv=ao.dsv(",","text/csv"),ao.tsv=ao.dsv(" ","text/tab-separated-values");var oa,aa,la,ca,fa=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ao.timer=function(){qn.apply(this,arguments)},ao.timer.flush=function(){Rn(),Dn()},ao.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var sa=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);ao.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=ao.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),sa[8+e/3]};var ha=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,pa=ao.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ao.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ga=ao.time={},va=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){da.setUTCDate.apply(this._,arguments)},setDay:function(){da.setUTCDay.apply(this._,arguments)},setFullYear:function(){da.setUTCFullYear.apply(this._,arguments)},setHours:function(){da.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){da.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){da.setUTCMinutes.apply(this._,arguments)},setMonth:function(){da.setUTCMonth.apply(this._,arguments)},setSeconds:function(){da.setUTCSeconds.apply(this._,arguments)},setTime:function(){da.setTime.apply(this._,arguments)}};var da=Date.prototype;ga.year=On(function(n){return n=ga.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ga.years=ga.year.range,ga.years.utc=ga.year.utc.range,ga.day=On(function(n){var t=new va(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ga.days=ga.day.range,ga.days.utc=ga.day.utc.range,ga.dayOfYear=function(n){var t=ga.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ga[n]=On(function(n){return(n=ga.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ga[n+"s"]=e.range,ga[n+"s"].utc=e.utc.range,ga[n+"OfYear"]=function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)}}),ga.week=ga.sunday,ga.weeks=ga.sunday.range,ga.weeks.utc=ga.sunday.utc.range,ga.weekOfYear=ga.sundayOfYear;var ya={"-":"",_:" ",0:"0"},ma=/^\s*\d+/,Ma=/^%/;ao.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xa=ao.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], + shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++rn?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),o.precision(t),a.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),o.scale(.35*t),a.scale(t),n.translate(u.translate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var c=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*c,s-.238*c],[f+.455*c,s+.238*c]]).stream(l).point,r=o.translate([f-.307*c,s+.201*c]).clipExtent([[f-.425*c+Uo,s+.12*c+Uo],[f-.214*c-Uo,s+.234*c-Uo]]).stream(l).point,i=a.translate([f-.205*c,s+.212*c]).clipExtent([[f-.214*c+Uo,s+.166*c+Uo],[f-.115*c-Uo,s+.234*c-Uo]]).stream(l).point,n},n.scale(1070)};var Oa,Ia,Ya,Za,Va,Xa,$a={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Ia=0,$a.lineStart=$t},polygonEnd:function(){$a.lineStart=$a.lineEnd=$a.point=b,Oa+=xo(Ia/2)}},Ba={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wa={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wa.lineStart=ne},polygonEnd:function(){Wa.point=Gt,Wa.lineStart=Kt,Wa.lineEnd=Qt}};ao.geo.path=function(){function n(n){return n&&("function"==typeof a&&u.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=i(u)),ao.geo.stream(n,o)),u.result()}function t(){return o=null,n}var e,r,i,u,o,a=4.5;return n.area=function(n){return Oa=0,ao.geo.stream(n,i($a)),Oa},n.centroid=function(n){return Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,i(Wa)),Ua?[Da/Ua,Pa/Ua]:Ra?[qa/Ra,Ta/Ra]:La?[Ca/La,za/La]:[NaN,NaN]},n.bounds=function(n){return Va=Xa=-(Ya=Za=1/0),ao.geo.stream(n,i(Ba)),[[Ya,Za],[Va,Xa]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||re(n):m,t()):e},n.context=function(n){return arguments.length?(u=null==(r=n)?new Wt:new te(n),"function"!=typeof a&&u.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(u.pointRadius(+t),+t),n):a},n.projection(ao.geo.albersUsa()).context(null)},ao.geo.transform=function(n){return{stream:function(t){var e=new ie(t);for(var r in n)e[r]=n[r];return e}}},ie.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ao.geo.projection=oe,ao.geo.projectionMutator=ae,(ao.geo.equirectangular=function(){return oe(ce)}).raw=ce.invert=ce,ao.geo.rotation=function(n){function t(t){return t=n(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t}return n=se(n[0]%360*Yo,n[1]*Yo,n.length>2?n[2]*Yo:0),t.invert=function(t){return t=n.invert(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t},t},fe.invert=ce,ao.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=se(-n[0]*Yo,-n[1]*Yo,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Zo,n[1]*=Zo}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Yo,i*Yo),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Yo,(i=+r)*Yo),n):i},n.angle(90)},ao.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Yo,i=n[1]*Yo,u=t[1]*Yo,o=Math.sin(r),a=Math.cos(r),l=Math.sin(i),c=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*o)*e+(e=c*f-l*s*a)*e),l*f+c*s*a)},ao.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ao.range(Math.ceil(u/d)*d,i,d).map(h).concat(ao.range(Math.ceil(c/y)*y,l,y).map(p)).concat(ao.range(Math.ceil(r/g)*g,e,g).filter(function(n){return xo(n%d)>Uo}).map(f)).concat(ao.range(Math.ceil(a/v)*v,o,v).filter(function(n){return xo(n%y)>Uo}).map(s))}var e,r,i,u,o,a,l,c,f,s,h,p,g=10,v=g,d=90,y=360,m=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(p(l).slice(1),h(i).reverse().slice(1),p(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],c=+t[0][1],l=+t[1][1],u>i&&(t=u,u=i,i=t),c>l&&(t=c,c=l,l=t),n.precision(m)):[[u,c],[i,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(m)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],y=+t[1],n):[d,y]},n.minorStep=function(t){return arguments.length?(g=+t[0],v=+t[1],n):[g,v]},n.precision=function(t){return arguments.length?(m=+t,f=ye(a,o,90),s=me(r,e,m),h=ye(c,l,90),p=me(u,i,m),n):m},n.majorExtent([[-180,-90+Uo],[180,90-Uo]]).minorExtent([[-180,-80-Uo],[180,80+Uo]])},ao.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Me,i=xe;return n.distance=function(){return ao.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},ao.geo.interpolate=function(n,t){return be(n[0]*Yo,n[1]*Yo,t[0]*Yo,t[1]*Yo)},ao.geo.length=function(n){return Ja=0,ao.geo.stream(n,Ga),Ja};var Ja,Ga={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ka=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ao.geo.azimuthalEqualArea=function(){return oe(Ka)}).raw=Ka;var Qa=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},m);(ao.geo.azimuthalEquidistant=function(){return oe(Qa)}).raw=Qa,(ao.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(ao.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(ao.geo.gnomonic=function(){return oe(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Io]},(ao.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(ao.geo.orthographic=function(){return oe(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ao.geo.stereographic=function(){return oe(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Io]},(ao.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,ao.geom={},ao.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i=En(e),u=En(r),o=n.length,a=[],l=[];for(t=0;o>t;t++)a.push([+i.call(this,n[t],t),+u.call(this,n[t],t),t]);for(a.sort(qe),t=0;o>t;t++)l.push([a[t][0],-a[t][1]]);var c=Le(a),f=Le(l),s=f[0]===c[0],h=f[f.length-1]===c[c.length-1],p=[];for(t=c.length-1;t>=0;--t)p.push(n[a[c[t]][2]]);for(t=+s;t=r&&c.x<=u&&c.y>=i&&c.y<=o?[[r,o],[u,o],[u,i],[r,i]]:[];f.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(u(n,t)/Uo)*Uo,y:Math.round(o(n,t)/Uo)*Uo,i:t}})}var r=Ce,i=ze,u=r,o=i,a=sl;return n?t(n):(t.links=function(n){return ar(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return ar(e(n)).cells.forEach(function(e,r){for(var i,u,o=e.site,a=e.edges.sort(Ve),l=-1,c=a.length,f=a[c-1].edge,s=f.l===o?f.r:f.l;++l=c,h=r>=f,p=h<<1|s;n.leaf=!1,n=n.nodes[p]||(n.nodes[p]=hr()),s?i=c:a=c,h?o=f:l=f,u(n,t,e,r,i,o,a,l)}var f,s,h,p,g,v,d,y,m,M=En(a),x=En(l);if(null!=t)v=t,d=e,y=r,m=i;else if(y=m=-(v=d=1/0),s=[],h=[],g=n.length,o)for(p=0;g>p;++p)f=n[p],f.xy&&(y=f.x),f.y>m&&(m=f.y),s.push(f.x),h.push(f.y);else for(p=0;g>p;++p){var b=+M(f=n[p],p),_=+x(f,p);v>b&&(v=b),d>_&&(d=_),b>y&&(y=b),_>m&&(m=_),s.push(b),h.push(_)}var w=y-v,S=m-d;w>S?m=d+w:y=v+S;var k=hr();if(k.add=function(n){u(k,n,+M(n,++p),+x(n,p),v,d,y,m)},k.visit=function(n){pr(n,k,v,d,y,m)},k.find=function(n){return gr(k,n[0],n[1],v,d,y,m)},p=-1,null==t){for(;++p=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||gl,r=dl.get(r)||m,br(r(e.apply(null,lo.call(arguments,1))))},ao.interpolateHcl=Rr,ao.interpolateHsl=Dr,ao.interpolateLab=Pr,ao.interpolateRound=Ur,ao.transform=function(n){var t=fo.createElementNS(ao.ns.prefix.svg,"g");return(ao.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:yl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var yl={a:1,b:0,c:0,d:1,e:0,f:0};ao.interpolateTransform=$r,ao.layout={},ao.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/y){if(v>l){var c=t.charge/l;n.px-=u*c,n.py-=o*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=u*c,n.py-=o*c}}return!t.charge}}function t(n){n.px=ao.event.x,n.py=ao.event.y,l.resume()}var e,r,i,u,o,a,l={},c=ao.dispatch("start","tick","end"),f=[1,1],s=.9,h=ml,p=Ml,g=-30,v=xl,d=.1,y=.64,M=[],x=[];return l.tick=function(){if((i*=.99)<.005)return e=null,c.end({type:"end",alpha:i=0}),!0;var t,r,l,h,p,v,y,m,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,p=l.target,m=p.x-h.x,b=p.y-h.y,(v=m*m+b*b)&&(v=i*o[r]*((v=Math.sqrt(v))-u[r])/v,m*=v,b*=v,p.x-=m*(y=h.weight+p.weight?h.weight/(h.weight+p.weight):.5),p.y-=b*y,h.x+=m*(y=1-y),h.y+=b*y);if((y=i*d)&&(m=f[0]/2,b=f[1]/2,r=-1,y))for(;++r<_;)l=M[r],l.x+=(m-l.x)*y,l.y+=(b-l.y)*y;if(g)for(ri(t=ao.geom.quadtree(M),i,a),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*s,l.y-=(l.py-(l.py=l.y))*s);c.tick({type:"tick",alpha:i})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(f=n,l):f},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.friction=function(n){return arguments.length?(s=+n,l):s},l.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(y=n*n,l):Math.sqrt(y)},l.alpha=function(n){return arguments.length?(n=+n,i?n>0?i=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:i=0})):n>0&&(c.start({type:"start",alpha:i=n}),e=qn(l.tick)),l):i},l.start=function(){function n(n,r){if(!e){for(e=new Array(i),l=0;i>l;++l)e[l]=[];for(l=0;c>l;++l){var u=x[l];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var o,a=e[t],l=-1,f=a.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;i>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",s)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof h)for(t=0;c>t;++t)u[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)u[t]=h;if(o=[],"function"==typeof p)for(t=0;c>t;++t)o[t]=+p.call(this,x[t],t);else for(t=0;c>t;++t)o[t]=p;if(a=[],"function"==typeof g)for(t=0;i>t;++t)a[t]=+g.call(this,M[t],t);else for(t=0;i>t;++t)a[t]=g;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=ao.behavior.drag().origin(m).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",ni)),arguments.length?void this.on("mouseover.force",ti).on("mouseout.force",ei).call(r):r},ao.rebind(l,c,"on")};var ml=20,Ml=1,xl=1/0;ao.layout.hierarchy=function(){function n(i){var u,o=[i],a=[];for(i.depth=0;null!=(u=o.pop());)if(a.push(u),(c=e.call(n,u,u.depth))&&(l=c.length)){for(var l,c,f;--l>=0;)o.push(f=c[l]),f.parent=u,f.depth=u.depth+1;r&&(u.value=0),u.children=c}else r&&(u.value=+r.call(n,u,u.depth)||0),delete u.children;return oi(i,function(n){var e,i;t&&(e=n.children)&&e.sort(t),r&&(i=n.parent)&&(i.value+=n.value)}),a}var t=ci,e=ai,r=li;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(ui(t,function(n){n.children&&(n.value=0)}),oi(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ao.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(o=u.length)){var o,a,l,c=-1;for(r=t.value?r/t.value:0;++cs?-1:1),g=ao.sum(c),v=g?(s-l*p)/g:0,d=ao.range(l),y=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(o[n],o[t])}),d.forEach(function(n){y[n]={data:o[n],value:a=c[n],startAngle:f,endAngle:f+=a*v+p,padAngle:h}}),y}var t=Number,e=bl,r=0,i=Ho,u=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n.padAngle=function(t){return arguments.length?(u=t,n):u},n};var bl={};ao.layout.stack=function(){function n(a,l){if(!(h=a.length))return a;var c=a.map(function(e,r){return t.call(n,e,r)}),f=c.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),o.call(n,t,e)]})}),s=e.call(n,f,l);c=ao.permute(c,s),f=ao.permute(f,s);var h,p,g,v,d=r.call(n,f,l),y=c[0].length;for(g=0;y>g;++g)for(i.call(n,c[0][g],v=d[g],f[0][g][1]),p=1;h>p;++p)i.call(n,c[p][g],v+=f[p-1][g][1],f[p][g][1]);return a}var t=m,e=gi,r=vi,i=pi,u=si,o=hi;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||gi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vi,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(i=t,n):i},n};var _l=ao.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(di),u=n.map(yi),o=ao.range(r).sort(function(n,t){return i[n]-i[t]}),a=0,l=0,c=[],f=[];for(t=0;r>t;++t)e=o[t],l>a?(a+=u[e],c.push(e)):(l+=u[e],f.push(e));return f.reverse().concat(c)},reverse:function(n){return ao.range(n.length).reverse()},"default":gi}),wl=ao.map({silhouette:function(n){var t,e,r,i=n.length,u=n[0].length,o=[],a=0,l=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;u>e;++e)l[e]=(a-o[e])/2;return l},wiggle:function(n){var t,e,r,i,u,o,a,l,c,f=n.length,s=n[0],h=s.length,p=[];for(p[0]=l=c=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,a=s[e][0]-s[e-1][0];f>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;u+=o*n[t][e][1]}p[e]=l-=i?u/i*a:0,c>l&&(c=l)}for(e=0;h>e;++e)p[e]-=c;return p},expand:function(n){var t,e,r,i=n.length,u=n[0].length,o=1/i,a=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=o}for(e=0;u>e;++e)a[e]=0;return a},zero:vi});ao.layout.histogram=function(){function n(n,u){for(var o,a,l=[],c=n.map(e,this),f=r.call(this,c,u),s=i.call(this,f,c,u),u=-1,h=c.length,p=s.length-1,g=t?1:1/h;++u0)for(u=-1;++u=f[0]&&a<=f[1]&&(o=l[ao.bisect(s,a,1,p)-1],o.y+=g,o.push(n[u]));return l}var t=!0,e=Number,r=bi,i=Mi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return xi(n,t)}:En(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ao.layout.pack=function(){function n(n,u){var o=e.call(this,n,u),a=o[0],l=i[0],c=i[1],f=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,oi(a,function(n){n.r=+f(n.value)}),oi(a,Ni),r){var s=r*(t?1:Math.max(2*a.r/l,2*a.r/c))/2;oi(a,function(n){n.r+=s}),oi(a,Ni),oi(a,function(n){n.r-=s})}return Ci(a,l/2,c/2,t?1:1/Math.max(2*a.r/l,2*a.r/c)),o}var t,e=ao.layout.hierarchy().sort(_i),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},ii(n,e)},ao.layout.tree=function(){function n(n,i){var f=o.call(this,n,i),s=f[0],h=t(s);if(oi(h,e),h.parent.m=-h.z,ui(h,r),c)ui(s,u);else{var p=s,g=s,v=s;ui(s,function(n){n.xg.x&&(g=n),n.depth>v.depth&&(v=n)});var d=a(p,g)/2-p.x,y=l[0]/(g.x+a(g,p)/2+d),m=l[1]/(v.depth||1);ui(s,function(n){n.x=(n.x+d)*y,n.y=n.depth*m})}return f}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var i,u=t.children,o=0,a=u.length;a>o;++o)r.push((u[o]=i={_:u[o],parent:t,children:(i=u[o].children)&&i.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=i);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Di(n);var u=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-u):n.z=u}else r&&(n.z=r.z+a(n._,r._));n.parent.A=i(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function i(n,t,e){if(t){for(var r,i=n,u=n,o=t,l=i.parent.children[0],c=i.m,f=u.m,s=o.m,h=l.m;o=Ti(o),i=qi(i),o&&i;)l=qi(l),u=Ti(u),u.a=n,r=o.z+s-i.z-c+a(o._,i._),r>0&&(Ri(Pi(o,n,e),n,r),c+=r,f+=r),s+=o.m,c+=i.m,h+=l.m,f+=u.m;o&&!Ti(u)&&(u.t=o,u.m+=s-f),i&&!qi(l)&&(l.t=i,l.m+=c-h,e=n)}return e}function u(n){n.x*=l[0],n.y=n.depth*l[1]}var o=ao.layout.hierarchy().sort(null).value(null),a=Li,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(c=null==(l=t)?u:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:u,n):c?l:null},ii(n,o)},ao.layout.cluster=function(){function n(n,u){var o,a=t.call(this,n,u),l=a[0],c=0;oi(l,function(n){var t=n.children;t&&t.length?(n.x=ji(t),n.y=Ui(t)):(n.x=o?c+=e(n,o):0,n.y=0,o=n)});var f=Fi(l),s=Hi(l),h=f.x-e(f,s)/2,p=s.x+e(s,f)/2;return oi(l,i?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(p-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),a}var t=ao.layout.hierarchy().sort(null).value(null),e=Li,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=null==(r=t),n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=null!=(r=t),n):i?r:null},ii(n,t)},ao.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++it?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var o,a,l,c=s(e),f=[],h=u.slice(),g=1/0,v="slice"===p?c.dx:"dice"===p?c.dy:"slice-dice"===p?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),f.area=0;(l=h.length)>0;)f.push(o=h[l-1]),f.area+=o.area,"squarify"!==p||(a=r(f,v))<=g?(h.pop(),g=a):(f.area-=f.pop().area,i(f,v,c,!1),v=Math.min(c.dx,c.dy),f.length=f.area=0,g=1/0);f.length&&(i(f,v,c,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,o=s(t),a=r.slice(),l=[];for(n(a,o.dx*o.dy/t.value),l.area=0;u=a.pop();)l.push(u),l.area+=u.area,null!=u.z&&(i(l,u.z?o.dx:o.dy,o,!a.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,o=-1,a=n.length;++oe&&(u=e),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*g/r,r/(t*u*g)):1/0}function i(n,t,e,r){var i,u=-1,o=n.length,a=e.x,c=e.y,f=t?l(n.area/t):0; + if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ue.dx)&&(f=e.dx);++ue&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-p," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",m,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",p," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-p," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hu,r=pu,i=su,u=ql,o=gu,a=vu,l=du;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(i=En(t),n):i},n.padRadius=function(t){return arguments.length?(u=t==ql?ql:En(t),n):u},n.startAngle=function(t){return arguments.length?(o=En(t),n):o},n.endAngle=function(t){return arguments.length?(a=En(t),n):a},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Io;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";ao.svg.line=function(){return Mu(m)};var Tl=ao.map({linear:xu,"linear-closed":bu,step:_u,"step-before":wu,"step-after":Su,basis:zu,"basis-open":Lu,"basis-closed":qu,bundle:Tu,cardinal:Eu,"cardinal-open":ku,"cardinal-closed":Nu,monotone:Fu});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];ao.svg.line.radial=function(){var n=Mu(Hu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wu.reverse=Su,Su.reverse=wu,ao.svg.area=function(){return Ou(m)},ao.svg.area.radial=function(){var n=Ou(Hu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ao.svg.chord=function(){function n(n,a){var l=t(this,u,n,a),c=t(this,o,n,a);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?i(l.r,l.p1,l.r,l.p0):i(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+i(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=a.call(n,i,r),o=l.call(n,i,r)-Io,f=c.call(n,i,r)-Io;return{r:u,a0:o,a1:f,p0:[u*Math.cos(o),u*Math.sin(o)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fo)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Me,o=xe,a=Iu,l=gu,c=vu;return n.radius=function(t){return arguments.length?(a=En(t),n):a},n.source=function(t){return arguments.length?(u=En(t),n):u},n.target=function(t){return arguments.length?(o=En(t),n):o},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},ao.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),o=e.call(this,n,i),a=(u.y+o.y)/2,l=[u,{x:u.x,y:a},{x:o.x,y:a},o];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yu;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ao.svg.diagonal.radial=function(){var n=ao.svg.diagonal(),t=Yu,e=n.projection;return n.projection=function(n){return arguments.length?e(Zu(t=n)):t},n},ao.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$u)(e.call(this,n,r))}var t=Xu,e=Vu;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=ao.map({circle:$u,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ao.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Yo);Co.transition=function(n){for(var t,e,r=Hl||++Zl,i=Ku(n),u=[],o=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},a=-1,l=this.length;++au;u++){i.push(t=[]);for(var e=this[u],a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return Wu(i,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(i){i[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?$r:Mr,a=ao.ns.qualify(n);return Ju(this,"attr."+n,t,a.local?u:i)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=ao.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Yl.style=function(n,e,r){function i(){this.style.removeProperty(n)}function u(e){return null==e?i:(e+="",function(){var i,u=t(this).getComputedStyle(this,null).getPropertyValue(n);return u!==e&&(i=Mr(u,e),function(t){this.style.setProperty(n,i(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ju(this,"style."+n,e,u)},Yl.styleTween=function(n,e,r){function i(i,u){var o=e.call(this,i,u,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,i)},Yl.text=function(n){return Ju(this,"text",n,Gu)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ao.ease.apply(ao,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,i,u){r[e][t].delay=+n.call(r,r.__data__,i,u)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,i,u){r[e][t].duration=Math.max(1,n.call(r,r.__data__,i,u))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var i=Ol,u=Hl;try{Hl=e,Y(this,function(t,i,u){Ol=t[r][e],n.call(t,t.__data__,i,u)})}finally{Ol=i,Hl=u}}else Y(this,function(i){var u=i[r][e];(u.event||(u.event=ao.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,i=this.id,u=++Zl,o=this.namespace,a=[],l=0,c=this.length;c>l;l++){a.push(n=[]);for(var t=this[l],f=0,s=t.length;s>f;f++)(e=t[f])&&(r=e[o][i],Qu(e,f,o,u,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wu(a,o,u)},ao.svg.axis=function(){function n(n){n.each(function(){var n,c=ao.select(this),f=this.__chart__||e,s=this.__chart__=e.copy(),h=null==l?s.ticks?s.ticks.apply(s,a):s.domain():l,p=null==t?s.tickFormat?s.tickFormat.apply(s,a):m:t,g=c.selectAll(".tick").data(h,s),v=g.enter().insert("g",".domain").attr("class","tick").style("opacity",Uo),d=ao.transition(g.exit()).style("opacity",Uo).remove(),y=ao.transition(g.order()).style("opacity",1),M=Math.max(i,0)+o,x=Zi(s),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ao.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=y.select("line"),C=g.select("text").text(p),z=v.select("text"),L=y.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=no,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*u+"V0H"+x[1]+"V"+q*u)):(n=to,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*u+","+x[0]+"H0V"+x[1]+"H"+q*u)),E.attr(N,q*i),z.attr(k,q*M),A.attr(S,0).attr(N,q*i),L.attr(w,0).attr(k,q*M),s.rangeBand){var T=s,R=T.rangeBand()/2;f=s=function(n){return T(n)+R}}else f.rangeBand?f=s:d.call(n,s,f);v.call(n,f,s),y.call(n,s,s)})}var t,e=ao.scale.linear(),r=Vl,i=6,u=6,o=3,a=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(a=co(arguments),n):a},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(i=+t,u=+arguments[e-1],n):i},n.innerTickSize=function(t){return arguments.length?(i=+t,n):i},n.outerTickSize=function(t){return arguments.length?(u=+t,n):u},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};ao.svg.brush=function(){function n(t){t.each(function(){var t=ao.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,m);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,s=ao.transition(t),h=ao.transition(o);c&&(l=Zi(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(s)),f&&(l=Zi(f),h.attr("y",l[0]).attr("height",l[1]-l[0]),i(s)),e(s)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",s[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1]-s[0])}function i(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function u(){function u(){32==ao.event.keyCode&&(C||(M=null,L[0]-=s[1],L[1]-=h[1],C=2),S())}function v(){32==ao.event.keyCode&&2==C&&(L[0]+=s[1],L[1]+=h[1],C=0,S())}function d(){var n=ao.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ao.event.altKey?(M||(M=[(s[0]+s[1])/2,(h[0]+h[1])/2]),L[0]=s[+(n[0]f?(i=r,r=f):i=f),v[0]!=r||v[1]!=i?(e?a=null:o=null,v[0]=r,v[1]=i,!0):void 0}function m(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ao.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ao.select(ao.event.target),w=l.of(b,arguments),k=ao.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&f,C=_.classed("extent"),z=W(b),L=ao.mouse(b),q=ao.select(t(b)).on("keydown.brush",u).on("keyup.brush",v);if(ao.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",m):q.on("mousemove.brush",d).on("mouseup.brush",m),k.interrupt().selectAll("*").interrupt(),C)L[0]=s[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[s[1-T]-L[0],h[1-R]-L[1]],L[0]=s[T],L[1]=h[R]}else ao.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ao.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,l=N(n,"brushstart","brush","brushend"),c=null,f=null,s=[0,0],h=[0,0],p=!0,g=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:s,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Hl?ao.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,s=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(s,t.x),r=xr(h,t.y);return o=a=null,function(i){s=t.x=e(i),h=t.y=r(i),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!f],n):c},n.y=function(t){return arguments.length?(f=t,v=Bl[!c<<1|!f],n):f},n.clamp=function(t){return arguments.length?(c&&f?(p=!!t[0],g=!!t[1]):c?p=!!t:f&&(g=!!t),n):c&&f?[p,g]:c?p:f?g:null},n.extent=function(t){var e,r,i,u,l;return arguments.length?(c&&(e=t[0],r=t[1],f&&(e=e[0],r=r[0]),o=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),e==s[0]&&r==s[1]||(s=[e,r])),f&&(i=t[0],u=t[1],c&&(i=i[1],u=u[1]),a=[i,u],f.invert&&(i=f(i),u=f(u)),i>u&&(l=i,i=u,u=l),i==h[0]&&u==h[1]||(h=[i,u])),n):(c&&(o?(e=o[0],r=o[1]):(e=s[0],r=s[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),f&&(a?(i=a[0],u=a[1]):(i=h[0],u=h[1],f.invert&&(i=f.invert(i),u=f.invert(u)),i>u&&(l=i,i=u,u=l))),c&&f?[[e,i],[r,u]]:c?[e,r]:f&&[i,u])},n.clear=function(){return n.empty()||(s=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!c&&s[0]==s[1]||!!f&&h[0]==h[1]},ao.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=ga.format=xa.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?eo:Gl,eo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},eo.toString=Gl.toString,ga.second=On(function(n){return new va(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ga.seconds=ga.second.range,ga.seconds.utc=ga.second.utc.range,ga.minute=On(function(n){return new va(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ga.minutes=ga.minute.range,ga.minutes.utc=ga.minute.utc.range,ga.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new va(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ga.hours=ga.hour.range,ga.hours.utc=ga.hour.utc.range,ga.month=On(function(n){return n=ga.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ga.months=ga.month.range,ga.months.utc=ga.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[ga.second,1],[ga.second,5],[ga.second,15],[ga.second,30],[ga.minute,1],[ga.minute,5],[ga.minute,15],[ga.minute,30],[ga.hour,1],[ga.hour,3],[ga.hour,6],[ga.hour,12],[ga.day,1],[ga.day,2],[ga.week,1],[ga.month,1],[ga.month,3],[ga.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return ao.range(Math.ceil(n/e)*e,+t,e).map(io)},floor:m,ceil:m};Ql.year=ga.year,ga.scale=function(){return ro(ao.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=ga.year.utc,ga.scale.utc=function(){return ro(ao.scale.linear(),ec,rc)},ao.text=An(function(n){return n.responseText}),ao.json=function(n,t){return Cn(n,"application/json",uo,t)},ao.html=function(n,t){return Cn(n,"text/html",oo,t)},ao.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=ao,define(ao)):"object"==typeof module&&module.exports?module.exports=ao:this.d3=ao}(); diff --git a/inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js b/inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js new file mode 100644 index 00000000..fa90f6e5 --- /dev/null +++ b/inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js @@ -0,0 +1,3 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e.Leaflet=e.Leaflet||{},e.Leaflet.markercluster=e.Leaflet.markercluster||{}))}(this,function(e){"use strict";var t=L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,clusterPane:L.Marker.prototype.options.pane,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,i=this._zoom;if(e.__parent)for(;t.__parent._zoom>=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):e.getLatLng?this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e}),this):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e}),this)},addLayers:function(e,t){if(!L.Util.isArray(e))return this.addLayer(e);var i,n=this._featureGroup,r=this._nonPointGroup,s=this.options.chunkedLoading,o=this.options.chunkInterval,a=this.options.chunkProgress,h=e.length,l=0,u=!0;if(this._map){var _=(new Date).getTime(),d=L.bind(function(){for(var c=(new Date).getTime();h>l;l++){if(s&&0===l%200){var p=(new Date).getTime()-c;if(p>o)break}if(i=e[l],i instanceof L.LayerGroup)u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length;else if(i.getLatLng){if(!this.hasLayer(i)&&(this._addLayer(i,this._maxZoom),t||this.fire("layeradd",{layer:i}),i.__parent&&2===i.__parent.getChildCount())){var f=i.__parent.getAllChildMarkers(),m=f[0]===i?f[1]:f[0];n.removeLayer(m)}}else r.addLayer(i),t||this.fire("layeradd",{layer:i})}a&&a(l,h,(new Date).getTime()-_),l===h?(this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else for(var c=this._needsClustering;h>l;l++)i=e[l],i instanceof L.LayerGroup?(u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length):i.getLatLng?this.hasLayer(i)||c.push(i):r.addLayer(i);return this},removeLayers:function(e){var t,i,n=e.length,r=this._featureGroup,s=this._nonPointGroup,o=!0;if(!this._map){for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):(this._arraySplice(this._needsClustering,i),s.removeLayer(i),this.hasLayer(i)&&this._needsRemoving.push({layer:i,latlng:i._latlng}),this.fire("layerremove",{layer:i}));return this}if(this._unspiderfy){this._unspiderfy();var a=e.slice(),h=n;for(t=0;h>t;t++)i=a[t],i instanceof L.LayerGroup?(this._extractNonGroupLayers(i,a),h=a.length):this._unspiderfyLayer(i)}for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):i.__parent?(this._removeLayer(i,!0,!0),this.fire("layerremove",{layer:i}),r.hasLayer(i)&&(r.removeLayer(i),i.clusterShow&&i.clusterShow())):(s.removeLayer(i),this.fire("layerremove",{layer:i}));return this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),this},clearLayers:function(){return this._map||(this._needsClustering=[],this._needsRemoving=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(e){e.off(this._childMarkerEventHandlers,this),delete e.__parent},this),this._map&&this._generateInitialClusters(),this},getBounds:function(){var e=new L.LatLngBounds;this._topClusterLevel&&e.extend(this._topClusterLevel._bounds);for(var t=this._needsClustering.length-1;t>=0;t--)e.extend(this._needsClustering[t].getLatLng());return e.extend(this._nonPointGroup.getBounds()),e},eachLayer:function(e,t){var i,n,r,s=this._needsClustering.slice(),o=this._needsRemoving;for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(s),n=s.length-1;n>=0;n--){for(i=!0,r=o.length-1;r>=0;r--)if(o[r].layer===s[n]){i=!1;break}i&&e.call(t,s[n])}this._nonPointGroup.eachLayer(e,t)},getLayers:function(){var e=[];return this.eachLayer(function(t){e.push(t)}),e},getLayer:function(e){var t=null;return e=parseInt(e,10),this.eachLayer(function(i){L.stamp(i)===e&&(t=i)}),t},hasLayer:function(e){if(!e)return!1;var t,i=this._needsClustering;for(t=i.length-1;t>=0;t--)if(i[t]===e)return!0;for(i=this._needsRemoving,t=i.length-1;t>=0;t--)if(i[t].layer===e)return!1;return!(!e.__parent||e.__parent._group!==this)||this._nonPointGroup.hasLayer(e)},zoomToShowLayer:function(e,t){"function"!=typeof t&&(t=function(){});var i=function(){!e._icon&&!e.__parent._icon||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),e._icon?t():e.__parent._icon&&(this.once("spiderfied",t,this),e.__parent.spiderfy()))};e._icon&&this._map.getBounds().contains(e.getLatLng())?t():e.__parent._zoomt;t++)n=this._needsRemoving[t],n.newlatlng=n.layer._latlng,n.layer._latlng=n.latlng;for(t=0,i=this._needsRemoving.length;i>t;t++)n=this._needsRemoving[t],this._removeLayer(n.layer,!0),n.layer._latlng=n.newlatlng;this._needsRemoving=[],this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i,!0)},onRemove:function(e){e.off("zoomend",this._zoomEnd,this),e.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),delete this._maxLat,this._hideCoverage(),this._featureGroup.remove(),this._nonPointGroup.remove(),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(e){for(var t=e;t&&!t._icon;)t=t.__parent;return t||null},_arraySplice:function(e,t){for(var i=e.length-1;i>=0;i--)if(e[i]===t)return e.splice(i,1),!0},_removeFromGridUnclustered:function(e,t){for(var i=this._map,n=this._gridUnclustered,r=Math.floor(this._map.getMinZoom());t>=r&&n[t].removeObject(e,i.project(e.getLatLng(),t));t--);},_childMarkerDragStart:function(e){e.target.__dragStart=e.target._latlng},_childMarkerMoved:function(e){if(!this._ignoreMove&&!e.target.__dragStart){var t=e.target._popup&&e.target._popup.isOpen();this._moveChild(e.target,e.oldLatLng,e.latlng),t&&e.target.openPopup()}},_moveChild:function(e,t,i){e._latlng=t,this.removeLayer(e),e._latlng=i,this.addLayer(e)},_childMarkerDragEnd:function(e){var t=e.target.__dragStart;delete e.target.__dragStart,t&&this._moveChild(e.target,t,e.target._latlng)},_removeLayer:function(e,t,i){var n=this._gridClusters,r=this._gridUnclustered,s=this._featureGroup,o=this._map,a=Math.floor(this._map.getMinZoom());t&&this._removeFromGridUnclustered(e,this._maxZoom);var h,l=e.__parent,u=l._markers;for(this._arraySplice(u,e);l&&(l._childCount--,l._boundsNeedUpdate=!0,!(l._zoomt?"small":100>t?"medium":"large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(t||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){for(var t=e.layer,i=t;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),e.layer.getChildCount()>2&&e.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(e.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(e||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),t&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,Math.round(this._map._zoom),e),this._currentShownBounds=e}},_generateInitialClusters:function(){var e=Math.ceil(this._map.getMaxZoom()),t=Math.floor(this._map.getMinZoom()),i=this.options.maxClusterRadius,n=i;"function"!=typeof i&&(n=function(){return i}),null!==this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var r=e;r>=t;r--)this._gridClusters[r]=new L.DistanceGrid(n(r)),this._gridUnclustered[r]=new L.DistanceGrid(n(r));this._topClusterLevel=new this._markerCluster(this,t-1)},_addLayer:function(e,t){var i,n,r=this._gridClusters,s=this._gridUnclustered,o=Math.floor(this._map.getMinZoom());for(this.options.singleMarkerMode&&this._overrideMarkerIcon(e),e.on(this._childMarkerEventHandlers,this);t>=o;t--){i=this._map.project(e.getLatLng(),t);var a=r[t].getNearObject(i);if(a)return a._addChild(e),e.__parent=a,void 0;if(a=s[t].getNearObject(i)){var h=a.__parent;h&&this._removeLayer(a,!1);var l=new this._markerCluster(this,t,a,e);r[t].addObject(l,this._map.project(l._cLatLng,t)),a.__parent=l,e.__parent=l;var u=l;for(n=t-1;n>h._zoom;n--)u=new this._markerCluster(this,n,u),r[n].addObject(u,this._map.project(a.getLatLng(),n));return h._addChild(u),this._removeFromGridUnclustered(a,t),void 0}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,n=e.getLayers(),r=0;for(t=t||[];r=0;i--)o=h[i],n.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,t),r.eachLayer(function(e){e instanceof L.MarkerCluster||!e._icon||e.clusterShow()}),this._topClusterLevel._recursively(n,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),this._ignoreMove=!1,this._enqueue(function(){this._topClusterLevel._recursively(n,e,s,function(e){r.removeLayer(e),e.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),e,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){var i=this,n=this._featureGroup;n.addLayer(e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.clusterHide(),this._enqueue(function(){n.removeLayer(e),e.clusterShow(),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(t,this._map.getMaxZoom(),this._zoom)))}},_animationZoomOutSingle:function(e,t,i){var n=this._getExpandedVisibleBounds(),r=Math.floor(this._map.getMinZoom());e._recursivelyAnimateChildrenInAndAddSelfToMap(n,r,t+1,i);var s=this;this._forceLayout(),e._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===e._childCount){var o=e._markers[0];this._ignoreMove=!0,o.setLatLng(o.getLatLng()),this._ignoreMove=!1,o.clusterShow&&o.clusterShow()}else e._recursively(n,i,r,function(e){e._recursivelyRemoveChildrenFromMap(n,r,t+1)});s._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}),L.markerClusterGroup=function(e){return new L.MarkerClusterGroup(e)};var i=L.MarkerCluster=L.Marker.extend({options:L.Icon.prototype.options,initialize:function(e,t,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this,pane:e.options.clusterPane}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(e,t){e=e||[];for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i].getAllChildMarkers(e);for(var n=this._markers.length-1;n>=0;n--)t&&this._markers[n].__dragStart||e.push(this._markers[n]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(e){for(var t,i=this._childClusters.slice(),n=this._group._map,r=n.getBoundsZoom(this._bounds),s=this._zoom+1,o=n.getZoom();i.length>0&&r>s;){s++;var a=[];for(t=0;ts?this._group._map.setView(this._latlng,s):o>=r?this._group._map.setView(this._latlng,o+1):this._group._map.fitBounds(this._bounds,e)},getBounds:function(){var e=new L.LatLngBounds;return e.extend(this._bounds),e},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_setClusterCenter:function(e){this._cLatLng||(this._cLatLng=e._cLatLng||e._latlng)},_resetBounds:function(){var e=this._bounds;e._southWest&&(e._southWest.lat=1/0,e._southWest.lng=1/0),e._northEast&&(e._northEast.lat=-1/0,e._northEast.lng=-1/0)},_recalculateBounds:function(){var e,t,i,n,r=this._markers,s=this._childClusters,o=0,a=0,h=this._childCount;if(0!==h){for(this._resetBounds(),e=0;e=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())},function(e){var i,n,r=e._childClusters;for(i=r.length-1;i>=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,i,n){this._recursively(e,n,t,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),i),r._isSingleParent()&&i-1===n?(r.clusterShow(),r._recursivelyRemoveChildrenFromMap(e,t,i)):r.clusterHide(),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,this._group._map.getMinZoom(),t,null,function(e){e.clusterShow()})},_recursivelyAddChildrenToMap:function(e,t,i){this._recursively(i,this._group._map.getMinZoom()-1,t,function(n){if(t!==n._zoom)for(var r=n._markers.length-1;r>=0;r--){var s=n._markers[r];i.contains(s._latlng)&&(e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.clusterHide&&s.clusterHide()),n._group._featureGroup.addLayer(s))}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var i=this._markers[t];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(e-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,i,n){var r,s;this._recursively(e,t-1,i-1,function(e){for(s=e._markers.length-1;s>=0;s--)r=e._markers[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())},function(e){for(s=e._childClusters.length-1;s>=0;s--)r=e._childClusters[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())})},_recursively:function(e,t,i,n,r){var s,o,a=this._childClusters,h=this._zoom;if(h>=t&&(n&&n(this),r&&h===i&&r(this)),t>h||i>h)for(s=a.length-1;s>=0;s--)o=a[s],o._boundsNeedUpdate&&o._recalculateBounds(),e.intersects(o._bounds)&&o._recursively(e,t,i,n,r)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}});L.Marker.include({clusterHide:function(){var e=this.options.opacity;return this.setOpacity(0),this.options.opacity=e,this},clusterShow:function(){return this.setOpacity(this.options.opacity)}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var i=this._getCoord(t.x),n=this._getCoord(t.y),r=this._grid,s=r[n]=r[n]||{},o=s[i]=s[i]||[],a=L.Util.stamp(e);this._objectPoint[a]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var i,n,r=this._getCoord(t.x),s=this._getCoord(t.y),o=this._grid,a=o[s]=o[s]||{},h=a[r]=a[r]||[];for(delete this._objectPoint[L.Util.stamp(e)],i=0,n=h.length;n>i;i++)if(h[i]===e)return h.splice(i,1),1===n&&delete a[r],!0},eachObject:function(e,t){var i,n,r,s,o,a,h,l=this._grid;for(i in l){o=l[i];for(n in o)for(a=o[n],r=0,s=a.length;s>r;r++)h=e.call(t,a[r]),h&&(r--,s--)}},getNearObject:function(e){var t,i,n,r,s,o,a,h,l=this._getCoord(e.x),u=this._getCoord(e.y),_=this._objectPoint,d=this._sqCellSize,c=null;for(t=u-1;u+1>=t;t++)if(r=this._grid[t])for(i=l-1;l+1>=i;i++)if(s=r[i])for(n=0,o=s.length;o>n;n++)a=s[n],h=this._sqDist(_[L.Util.stamp(a)],e),(d>h||d>=h&&null===c)&&(d=h,c=a);return c},_getCoord:function(e){var t=Math.floor(e/this._cellSize);return isFinite(t)?t:e},_sqDist:function(e,t){var i=t.x-e.x,n=t.y-e.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(e,t){var i=t[1].lat-t[0].lat,n=t[0].lng-t[1].lng;return n*(e.lat-t[0].lat)+i*(e.lng-t[0].lng)},findMostDistantPointFromBaseLine:function(e,t){var i,n,r,s=0,o=null,a=[];for(i=t.length-1;i>=0;i--)n=t[i],r=this.getDistant(n,e),r>0&&(a.push(n),r>s&&(s=r,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(e,t){var i=[],n=this.findMostDistantPointFromBaseLine(e,t);return n.maxPoint?(i=i.concat(this.buildConvexHull([e[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,e[1]],n.newPoints))):[e[0]]},getConvexHull:function(e){var t,i=!1,n=!1,r=!1,s=!1,o=null,a=null,h=null,l=null,u=null,_=null;for(t=e.length-1;t>=0;t--){var d=e[t];(i===!1||d.lat>i)&&(o=d,i=d.lat),(n===!1||d.latr)&&(h=d,r=d.lng),(s===!1||d.lng=0;t--)e=i[t].getLatLng(),n.push(e);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:0,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var e,t=this.getAllChildMarkers(null,!0),i=this._group,n=i._map,r=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,t.length>=this._circleSpiralSwitchover?e=this._generatePointsSpiral(t.length,r):(r.y+=10,e=this._generatePointsCircle(t.length,r)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,n,r=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),s=r/this._2PI,o=this._2PI/e,a=[];for(s=Math.max(s,35),a.length=e,i=0;e>i;i++)n=this._circleStartAngle+i*o,a[i]=new L.Point(t.x+s*Math.cos(n),t.y+s*Math.sin(n))._round();return a},_generatePointsSpiral:function(e,t){var i,n=this._group.options.spiderfyDistanceMultiplier,r=n*this._spiralLengthStart,s=n*this._spiralFootSeparation,o=n*this._spiralLengthFactor*this._2PI,a=0,h=[];for(h.length=e,i=e;i>=0;i--)e>i&&(h[i]=new L.Point(t.x+r*Math.cos(a),t.y+r*Math.sin(a))._round()),a+=s/r+5e-4*i,r+=o/a;return h},_noanimationUnspiderfy:function(){var e,t,i=this._group,n=i._map,r=i._featureGroup,s=this.getAllChildMarkers(null,!0);for(i._ignoreMove=!0,this.setOpacity(1),t=s.length-1;t>=0;t--)e=s[t],r.removeLayer(e),e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng),e.setZIndexOffset&&e.setZIndexOffset(0),e._spiderLeg&&(n.removeLayer(e._spiderLeg),delete e._spiderLeg);i.fire("unspiderfied",{cluster:this,markers:s}),i._ignoreMove=!1,i._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(e,t){var i,n,r,s,o=this._group,a=o._map,h=o._featureGroup,l=this._group.options.spiderLegPolylineOptions;for(o._ignoreMove=!0,i=0;i=0;i--)a=u.layerPointToLatLng(t[i]),n=e[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(a),n.clusterShow&&n.clusterShow(),p&&(r=n._spiderLeg,s=r._path,s.style.strokeDashoffset=0,r.setStyle({opacity:m}));this.setOpacity(.3),l._ignoreMove=!1,setTimeout(function(){l._animationEnd(),l.fire("spiderfied",{cluster:h,markers:e})},200)},_animationUnspiderfy:function(e){var t,i,n,r,s,o,a=this,h=this._group,l=h._map,u=h._featureGroup,_=e?l._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):l.latLngToLayerPoint(this._latlng),d=this.getAllChildMarkers(null,!0),c=L.Path.SVG;for(h._ignoreMove=!0,h._animationStart(),this.setOpacity(1),i=d.length-1;i>=0;i--)t=d[i],t._preSpiderfyLatlng&&(t.closePopup(),t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng,o=!0,t._setPos&&(t._setPos(_),o=!1),t.clusterHide&&(t.clusterHide(),o=!1),o&&u.removeLayer(t),c&&(n=t._spiderLeg,r=n._path,s=r.getTotalLength()+.1,r.style.strokeDashoffset=s,n.setStyle({opacity:0})));h._ignoreMove=!1,setTimeout(function(){var e=0;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&e++;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&(t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),e>1&&u.removeLayer(t),l.removeLayer(t._spiderLeg),delete t._spiderLeg);h._animationEnd(),h.fire("unspiderfied",{cluster:a,markers:d})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,unspiderfy:function(){this._unspiderfy.apply(this,arguments)},_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Browser.touch||this._map.getRenderer(this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy() +},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(e){e._spiderLeg&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}}),L.MarkerClusterGroup.include({refreshClusters:function(e){return e?e instanceof L.MarkerClusterGroup?e=e._topClusterLevel.getAllChildMarkers():e instanceof L.LayerGroup?e=e._layers:e instanceof L.MarkerCluster?e=e.getAllChildMarkers():e instanceof L.Marker&&(e=[e]):e=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(e),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(e),this},_flagParentsIconsNeedUpdate:function(e){var t,i;for(t in e)for(i=e[t].__parent;i;)i._iconNeedsUpdate=!0,i=i.__parent},_refreshSingleMarkerModeMarkers:function(e){var t,i;for(t in e)i=e[t],this.hasLayer(i)&&i.setIcon(this._overrideMarkerIcon(i))}}),L.Marker.include({refreshIconOptions:function(e,t){var i=this.options.icon;return L.setOptions(i,e),this.setIcon(i),t&&this.__parent&&this.__parent._group.refreshClusters(this),this}}),e.MarkerClusterGroup=t,e.MarkerCluster=i}); +//# sourceMappingURL=leaflet.markercluster.js.map diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js new file mode 100644 index 00000000..7a7b541e --- /dev/null +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -0,0 +1,241 @@ +/* global LeafletWidget, $, L */ +LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, size, + popup, popupOptions, label, labelOptions, + clusterOptions, clusterId, + categoryField, categoryMap, popupFields, popupLabels, + markerOptions, legendOptions) { + + var map = this; + + var markerclusters = L.markerClusterGroup( + Object.assign({ + maxClusterRadius: 2 * rmax, + iconCreateFunction: defineClusterIcon // this is where the magic happens + }, clusterOptions) + ) + + map.addLayer(markerclusters); + map.layerManager.addLayer(markerclusters, "cluster", clusterId, group); + + console.log("geojson"); console.log(geojson) + var markers = L.geoJson(geojson, { + pointToLayer: defineFeature, + onEachFeature: defineFeaturePopup + }); + markerclusters.addLayer(markers); + map.fitBounds(markers.getBounds()); + renderLegend(); + + map.on('overlayadd', function(eventlayer){ + if (eventlayer.name == group) { + $(".clusterlegend").show() + } + }); + map.on('overlayremove', function(eventlayer){ + if (eventlayer.name == group) { + $(".clusterlegend").hide() + } + }); + + function defineFeature(feature, latlng) { + var categoryVal = feature.properties[categoryField] + var myClass = 'marker category-'+categoryVal+' icon-'+categoryVal; + //console.log("myClass"); console.log(myClass) + let extraInfo = { clusterId: clusterId }; + + // Make DIV-Icon marker + var myIcon = L.divIcon({ + className: myClass, + iconSize: null + }); + var marker = L.marker(latlng, + Object.assign({ + icon: myIcon + }, markerOptions) + ); + + // Add Labels + if (label !== null && feature.properties[label] && feature.properties[label] !== null) { + var labelshow = feature.properties[label]; + if (labelOptions !== null) { + if(labelOptions.permanent) { + marker.bindTooltip(labelshow, labelOptions).openTooltip(); + } else { + marker.bindTooltip(labelshow, labelOptions); + } + } else { + marker.bindTooltip(labelshow); + } + } + // Add Mouse events with layerId and Group + var lid = feature.properties[layerId] ? feature.properties[layerId] : layerId + var lgr = feature.properties[group] ? feature.properties[group] : group + marker.on("click", LeafletWidget.methods.mouseHandler(map.id, lid, lgr, "marker_click", extraInfo), this); + marker.on("mouseover", LeafletWidget.methods.mouseHandler(map.id, lid, lgr, "marker_mouseover", extraInfo), this); + marker.on("mouseout", LeafletWidget.methods.mouseHandler(map.id, lid, lgr, "marker_mouseout", extraInfo), this); + marker.on("dragend", LeafletWidget.methods.mouseHandler(map.id, lid, lgr, "marker_dragend", extraInfo), this); + + return marker; + } + function defineFeaturePopup(feature, layer) { + var props = feature.properties, + popupContent = ''; + if (popup && props[popup]) { + popupContent = props[popup]; + } else { + popupContent += ''; + popupFields.map( function(key, idx) { + if (props[key]) { + var val = props[key], + label = popupLabels[idx]; + popupContent += ''; + } + }); + popupContent += '
' + label + ':' + val + '
'; + } + if (popupOptions !== null){ + layer.bindPopup(popupContent, popupOptions); + } else { + layer.bindPopup(popupContent); + } + } + function defineClusterIcon(cluster) { + var children = cluster.getAllChildMarkers(), + n = children.length, //Get number of markers in cluster + strokeWidth = 1, //Set clusterpie stroke width + r = rmax-2*strokeWidth-(n<10?12:n<100?8:n<1000?4:0), //Calculate clusterpie radius... + iconDim = (r+strokeWidth)*2, //...and divIcon dimensions (leaflet really want to know the size) + data = d3.nest() //Build a dataset for the pie chart + .key(function(d) { return d.feature.properties[categoryField]; }) + .entries(children, d3.map), + //bake some svg markup + html = bakeThePie({data: data, + valueFunc: function(d){return d.values.length;}, + strokeWidth: 1, + outerRadius: r, + innerRadius: r-10, + pieClass: 'cluster-pie', + pieLabel: n, + pieLabelClass: 'marker-cluster-pie-label', + pathClassFunc: function(d){ + return "category-"+d.data.key; + }, + pathTitleFunc: function(d){ + return d.data.key+' ('+d.data.values.length+' element)'; + } + }), + //Create a new divIcon and assign the svg markup to the html property + myIcon = new L.DivIcon({ + html: html, + className: 'marker-cluster', + iconSize: new L.Point(iconDim, iconDim) + }); + + //console.log("data"); console.log(data) + return myIcon; + } + //function that generates a svg markup for the pie chart + function bakeThePie(options) { + //data and valueFunc are required + if (!options.data || !options.valueFunc) { + return ''; + } + var data = options.data, + valueFunc = options.valueFunc, + r = options.outerRadius?options.outerRadius:28, //Default outer radius = 28px + rInner = options.innerRadius?options.innerRadius:r-10, //Default inner radius = r-10 + strokeWidth = options.strokeWidth?options.strokeWidth:1, //Default stroke is 1 + pathClassFunc = options.pathClassFunc?options.pathClassFunc:function(){return '';}, //Class for each path + pathTitleFunc = options.pathTitleFunc?options.pathTitleFunc:function(){return '';}, //Title for each path + pieClass = options.pieClass?options.pieClass:'marker-cluster-pie', //Class for the whole pie + pieLabel = options.pieLabel?options.pieLabel:d3.sum(data,valueFunc), //Label for the whole pie + pieLabelClass = options.pieLabelClass?options.pieLabelClass:'marker-cluster-pie-label',//Class for the pie label + + origo = (r+strokeWidth), //Center coordinate + w = origo*2, //width and height of the svg element + h = w, + donut = d3.layout.pie(), + arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); + + //Create an svg element + var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); + //Create the pie chart + var vis = d3.select(svg) + .data([data]) + .attr('class', pieClass) + .attr('width', w) + .attr('height', h); + + var arcs = vis.selectAll('g.arc') + .data(donut.value(valueFunc)) + .enter().append('svg:g') + .attr('class', 'arc') + .attr('transform', 'translate(' + origo + ',' + origo + ')'); + + console.log("pathTitleFunc"); console.log(pathTitleFunc) + arcs.append('svg:path') + .attr('class', pathClassFunc) + .attr('stroke-width', strokeWidth) + .attr('d', arc) + .append('svg:title') + .text(pathTitleFunc); + + vis.append('text') + .attr('x',origo) + .attr('y',origo) + .attr('class', pieLabelClass) + .attr('text-anchor', 'middle') + //.attr('dominant-baseline', 'central') + //IE doesn't seem to support dominant-baseline, but setting dy to .3em does the trick + .attr('dy','.3em') + .text(pieLabel); + //Return the svg-markup rather than the actual element + return serializeXmlNode(svg); + } + //Helper function + function serializeXmlNode(xmlNode) { + if (typeof window.XMLSerializer != "undefined") { + return (new window.XMLSerializer()).serializeToString(xmlNode); + } else if (typeof xmlNode.xml != "undefined") { + return xmlNode.xml; + } + return ""; + } + + //Function for generating a legend with the same categories as in the clusterPie + function renderLegend() { + var data = Object.entries(categoryMap).map(([key, value]) => ({ + key: key, + value: value + })); + var legendControl = L.control({position: legendOptions.position}); + + legendControl.onAdd = function(map) { + var div = L.DomUtil.create('div', 'clusterlegend'); + div.innerHTML = '
' + legendOptions.title + '
'; + + var legendItems = d3.select(div) + .selectAll('.legenditem') + .data(data); + + legendItems.enter() + .append('div') + .attr('class', function(d) { + console.log("d"); console.log(d) + return 'category-' + d.value; + }) + .classed('legenditem', true) + .text(function(d) { return d.value; }); + + return div; + }; + + // Add the custom control to the map + legendControl.addTo(map); + } + +}; + + + + diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css new file mode 100644 index 00000000..c7f7100d --- /dev/null +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css @@ -0,0 +1,52 @@ + +.clustermarkerlabel { + font-weight: 800; +} + +.clusterlegend { + padding: 2px 9px; + border-radius: 8px; + z-index: 100; + font-size: 1em; + font-family: sans-serif; + background: rgba(255,255,255,0.9); + min-width: 100px; +} +.legendheading { + position: relative; + height: 25px; + padding: 5px 2px 0px 2px; + font-size: larger; + font-weight: bold; +} +.legenditem { + padding: 2px; + margin-bottom: 2px; +} + +/*Marker clusters*/ +.marker-cluster-pie g.arc{ + fill-opacity: 0.5; +} +.marker-cluster-pie-label { + font-size: 14px; + font-weight: bold; + font-family: sans-serif; +} + +/*Markers*/ +.marker { + border-width: 2px; + border-radius:10px; + margin-top: -10px; + margin-left: -10px; + border-style: solid; +} +.marker div{ + text-align: center; + font-size: 14px; + font-weight: bold; + font-family: sans-serif; +} + + diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd new file mode 100644 index 00000000..677394d6 --- /dev/null +++ b/man/addClusterCharts.Rd @@ -0,0 +1,85 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/clusterCharts.R +\name{addClusterCharts} +\alias{addClusterCharts} +\title{addClusterCharts} +\usage{ +addClusterCharts( + map, + lng = NULL, + lat = NULL, + layerId = NULL, + group = NULL, + rmax = 30, + size = c(18, 18), + popup = NULL, + popupOptions = NULL, + label = NULL, + labelOptions = NULL, + clusterOptions = NULL, + clusterId = NULL, + categoryField, + categoryMap, + popupFields = NULL, + popupLabels = NULL, + markerOptions = NULL, + legendOptions = list(title = "", position = "topright"), + data = getMapData(map) +) +} +\arguments{ +\item{map}{a map widget object created from \code{\link[leaflet]{leaflet}()}} + +\item{lng}{a numeric vector of longitudes, or a one-sided formula of the form +\code{~x} where \code{x} is a variable in \code{data}; by default (if not +explicitly provided), it will be automatically inferred from \code{data} by +looking for a column named \code{lng}, \code{long}, or \code{longitude} +(case-insensitively)} + +\item{lat}{a vector of latitudes or a formula (similar to the \code{lng} +argument; the names \code{lat} and \code{latitude} are used when guessing +the latitude column from \code{data})} + +\item{layerId}{the layer id} + +\item{group}{the name of the group the newly created layers should belong to +(for \code{\link[leaflet]{clearGroup}} and \code{\link[leaflet]{addLayersControl}} purposes). +Human-friendly group names are permitted--they need not be short, +identifier-style names. Any number of layers and even different types of +layers (e.g. markers and polygons) can share the same group name.} + +\item{rmax}{The maxClusterRadius} + +\item{popup}{a character vector of the HTML content for the popups (you are +recommended to escape the text using \code{\link[htmltools]{htmlEscape}()} +for security reasons)} + +\item{popupOptions}{A Vector of \code{\link[leaflet]{popupOptions}} to provide popups} + +\item{label}{a character vector of the HTML content for the labels} + +\item{labelOptions}{A Vector of \code{\link[leaflet]{labelOptions}} to provide label +options for each label. Default \code{NULL}} + +\item{clusterOptions}{if not \code{NULL}, markers will be clustered using +\href{https://github.com/Leaflet/Leaflet.markercluster}{Leaflet.markercluster}; + you can use \code{\link[leaflet]{markerClusterOptions}()} to specify marker cluster +options} + +\item{clusterId}{the id for the marker cluster layer} + +\item{categoryField}{in meters} + +\item{popupFields}{in meters} + +\item{data}{the data object from which the argument values are derived; by +default, it is the \code{data} object provided to \code{leaflet()} +initially, but can be overridden} + +\item{lat_center, lng_center}{lat/lng for the center} + +\item{radius}{in meters} +} +\description{ +Adds a Great Circle to the map. +} From fe0655c5c68ddb7f6837dc5cd5893d6153c09a14 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 03:14:39 +0200 Subject: [PATCH 02/26] center marker depending on size, rename --- R/clusterCharts.R | 5 ++++- .../lfx-clustercharts-bindings.js | 16 ++++++++-------- .../lfx-clustercharts/lfx-clustercharts.css | 12 +++++------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 4779c029..05dcf4c8 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -54,7 +54,10 @@ addClusterCharts <- function( # browser() css <- paste(apply(categoryMap, 1, generate_css), collapse = "\n") if (length(size) == 1) size <- rep(size, 2) - css <- paste0(css, "\n.marker {width: ",size[1],"px;height: ",size[1],"px;}") + css <- paste0(css, "\n.clustermarker {", + "width: ",size[1],"px; height: ",size[2],"px;", + "margin-top: -",size[1]/2,"px; margin-left: -",size[2]/2,"px;", + "}") csssrc <- list( htmltools::htmlDependency( "lfx-clustercharts-css", version = "1.0.0", diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 7a7b541e..8dd6f0f3 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -39,7 +39,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, function defineFeature(feature, latlng) { var categoryVal = feature.properties[categoryField] - var myClass = 'marker category-'+categoryVal+' icon-'+categoryVal; + var myClass = 'clustermarker category-'+categoryVal+' icon-'+categoryVal; //console.log("myClass"); console.log(myClass) let extraInfo = { clusterId: clusterId }; @@ -88,7 +88,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, if (props[key]) { var val = props[key], label = popupLabels[idx]; - popupContent += '' + label + ':' + val + ''; + popupContent += '' + label + ':' + val + ''; } }); popupContent += ''; @@ -111,26 +111,27 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, //bake some svg markup html = bakeThePie({data: data, valueFunc: function(d){return d.values.length;}, - strokeWidth: 1, + strokeWidth: strokeWidth, outerRadius: r, innerRadius: r-10, pieClass: 'cluster-pie', pieLabel: n, - pieLabelClass: 'marker-cluster-pie-label', + pieLabelClass: 'clustermarker-cluster-pie-label', pathClassFunc: function(d){ - return "category-"+d.data.key; + return "category-" + d.data.key; }, pathTitleFunc: function(d){ - return d.data.key+' ('+d.data.values.length+' element)'; + return d.data.key + ' (' + d.data.values.length + ')'; } }), //Create a new divIcon and assign the svg markup to the html property myIcon = new L.DivIcon({ html: html, - className: 'marker-cluster', + className: 'clustermarker-cluster', iconSize: new L.Point(iconDim, iconDim) }); + console.log("r"); console.log(r) //console.log("data"); console.log(data) return myIcon; } @@ -172,7 +173,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, .attr('class', 'arc') .attr('transform', 'translate(' + origo + ',' + origo + ')'); - console.log("pathTitleFunc"); console.log(pathTitleFunc) arcs.append('svg:path') .attr('class', pathClassFunc) .attr('stroke-width', strokeWidth) diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css index c7f7100d..5ea17472 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css @@ -1,5 +1,5 @@ -.clustermarkerlabel { +.clustermarkerpopuplabel { font-weight: 800; } @@ -25,24 +25,22 @@ } /*Marker clusters*/ -.marker-cluster-pie g.arc{ +.clustermarker-cluster-pie g.arc{ fill-opacity: 0.5; } -.marker-cluster-pie-label { +.clustermarker-cluster-pie-label { font-size: 14px; font-weight: bold; font-family: sans-serif; } /*Markers*/ -.marker { +.clustermarker { border-width: 2px; border-radius:10px; - margin-top: -10px; - margin-left: -10px; border-style: solid; } -.marker div{ +.clustermarker div{ text-align: center; font-size: 14px; font-weight: bold; From 4ca1e6eaf455dd526d51efb2363faab56101ebd2 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 04:00:54 +0200 Subject: [PATCH 03/26] working barchart --- R/clusterCharts.R | 7 +- inst/examples/clusterCharts_app.R | 8 +- .../lfx-clustercharts-bindings.js | 142 +++++++++++++++--- .../lfx-clustercharts/lfx-clustercharts.css | 2 +- man/addClusterCharts.Rd | 1 + 5 files changed, 128 insertions(+), 32 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 05dcf4c8..8b25d0fa 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -30,7 +30,7 @@ clusterchartsDependencies <- function() { #' @inheritParams leaflet::addCircleMarkers #' @export addClusterCharts <- function( - map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, + map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar"), rmax = 30, size = c(18, 18), popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, clusterOptions = NULL, clusterId = NULL, @@ -39,7 +39,8 @@ addClusterCharts <- function( position = "topright"), data = getMapData(map)) { - ## Check labelOptions, popupFields, popupLabels, clusterOptions ############ + ## Check type, labelOptions, popupFields, popupLabels, clusterOptions ############ + type <- match.arg(type) if (missing(labelOptions)) labelOptions <- labelOptions() if (!is.null(popupFields) && is.null(popupLabels)) { @@ -77,7 +78,7 @@ addClusterCharts <- function( points <- derivePoints(data, lng, lat, missing(lng), missing(lat), "addClusterCharts") leaflet::invokeMethod( - map, NULL, "addClusterCharts", geojson, layerId, group, rmax, size, + map, NULL, "addClusterCharts", geojson, layerId, group, type, rmax, size, popup, popupOptions, safeLabel(label, data), labelOptions, clusterOptions, clusterId, categoryField, categoryMap, popupFields, popupLabels, diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index f1e6336d..9797f5e2 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -29,10 +29,11 @@ server <- function(input, output, session) { addProviderTiles("CartoDB") %>% leaflet::addLayersControl(overlayGroups = "clustermarkers") %>% # addCircleMarkers(data = data, clusterOptions = markerClusterOptions()) %>% - # addCircleMarkers(data = data) %>% + # addCircleMarkers(data = data, options = pathOptions(pane = "clusterpane")) %>% addClusterCharts(data = data , rmax = 50 , size = 50 + , type = "bar" , categoryField = "category" , categoryMap = data.frame(label = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), @@ -68,8 +69,9 @@ server <- function(input, output, session) { removeOutsideVisibleBounds = TRUE, spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5), freezeAtZoom = TRUE, - spiderfyDistanceMultiplier = 2, - clusterPane = "clusterpane") + # clusterPane = "clusterpane", + spiderfyDistanceMultiplier = 2 + ) , labelOptions = labelOptions(opacity = 0.8, textsize = "14px") , popupOptions = popupOptions(maxWidth = 900, minWidth = 200, keepInView = TRUE) ) diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 8dd6f0f3..3cd08649 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -1,5 +1,5 @@ /* global LeafletWidget, $, L */ -LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, size, +LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, rmax, size, popup, popupOptions, label, labelOptions, clusterOptions, clusterId, categoryField, categoryMap, popupFields, popupLabels, @@ -105,27 +105,59 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, strokeWidth = 1, //Set clusterpie stroke width r = rmax-2*strokeWidth-(n<10?12:n<100?8:n<1000?4:0), //Calculate clusterpie radius... iconDim = (r+strokeWidth)*2, //...and divIcon dimensions (leaflet really want to know the size) - data = d3.nest() //Build a dataset for the pie chart - .key(function(d) { return d.feature.properties[categoryField]; }) - .entries(children, d3.map), + html; //bake some svg markup - html = bakeThePie({data: data, - valueFunc: function(d){return d.values.length;}, - strokeWidth: strokeWidth, - outerRadius: r, - innerRadius: r-10, - pieClass: 'cluster-pie', - pieLabel: n, - pieLabelClass: 'clustermarker-cluster-pie-label', - pathClassFunc: function(d){ - return "category-" + d.data.key; - }, - pathTitleFunc: function(d){ - return d.data.key + ' (' + d.data.values.length + ')'; - } - }), - //Create a new divIcon and assign the svg markup to the html property - myIcon = new L.DivIcon({ + var data = d3.nest() //Build a dataset for the pie chart + .key(function(d) { return d.feature.properties[categoryField]; }) + .entries(children, d3.map) + + // Group data by categoryField + /* + var data = Array.from( + d3.group(children, d => d.feature.properties[categoryField]), + ([key, values]) => ({ key, values }) + ); + */ + + if (type == "pie") { + console.log("Piechart") + html = bakeThePie({ + data: data, + valueFunc: function(d){return d.values.length;}, + strokeWidth: strokeWidth, + outerRadius: r, + innerRadius: r-10, + pieClass: 'cluster-pie', + pieLabel: n, + pieLabelClass: 'clustermarker-cluster-pie-label', + pathClassFunc: function(d){ + return "category-" + d.data.key; + }, + pathTitleFunc: function(d){ + return d.data.key + ' (' + d.data.values.length + ')'; + } + }) + } else { + console.log("Barchart") + html = bakeTheBarChart({ + data: data, + valueFunc: function(d){return d.values.length;}, + barClass: 'cluster-bar', + barLabel: n, + barLabelClass: 'clustermarker-cluster-bar-label', + pathClassFunc: function(d){ + console.log("d.data"); console.log(d.data) + //return "category-" + d.data.key; + }, + pathTitleFunc: function(d){ + console.log("d.data"); console.log(d.data) + //return d.data.key + ' (' + d.data.values.length + ')'; + } + }); + } + + //Create a new divIcon and assign the svg markup to the html property + var myIcon = new L.DivIcon({ html: html, className: 'clustermarker-cluster', iconSize: new L.Point(iconDim, iconDim) @@ -135,7 +167,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, //console.log("data"); console.log(data) return myIcon; } - //function that generates a svg markup for the pie chart + //function that generates a svg markup for the Pie chart function bakeThePie(options) { //data and valueFunc are required if (!options.data || !options.valueFunc) { @@ -178,20 +210,80 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, rmax, .attr('stroke-width', strokeWidth) .attr('d', arc) .append('svg:title') - .text(pathTitleFunc); + .text(pathTitleFunc); vis.append('text') .attr('x',origo) .attr('y',origo) .attr('class', pieLabelClass) .attr('text-anchor', 'middle') - //.attr('dominant-baseline', 'central') - //IE doesn't seem to support dominant-baseline, but setting dy to .3em does the trick .attr('dy','.3em') .text(pieLabel); + //Return the svg-markup rather than the actual element return serializeXmlNode(svg); } + + //function that generates a svg markup for the Bar chart + function bakeTheBarChart(options) { + if (!options.data || !options.valueFunc) { + return ''; + } + var data = options.data, + valueFunc = options.valueFunc, + barClass = options.barClass ? options.barClass : 'marker-cluster-bar', + barLabel = options.barLabel ? options.barLabel : d3.sum(data, function(d) { return d.values.length; }), + barLabelClass = options.barLabelClass ? options.barLabelClass : 'marker-cluster-bar-label', + pathClassFunc = options.pathClassFunc?options.pathClassFunc:function(){return '';}, + pathTitleFunc = options.pathTitleFunc?options.pathTitleFunc:function(){return '';}, + width = 100, + height = 50, + x = d3.scale.ordinal().rangeRoundBands([0, width], 0.1), + y = d3.scale.linear().range([height, 0]); + + x.domain(data.map(function(d) { return d.key; })); + y.domain([0, d3.max(data, function(d) { return d.values.length; })]); + + var svg = document.createElementNS(d3.ns.prefix.svg, "svg"); + var vis = d3.select(svg) + .attr('class', barClass) + .attr('width', width) + .attr('height', height); + + vis.selectAll('.bar') + .data(data) + .enter().append('rect') + //.attr('class', 'bar') + .attr('class', function(d) { + console.log("category-"+d.key) + return "category-"+d.key + }) + //.attr('class', pathClassFunc) + .attr('x', function(d) { + console.log("vis.selectAll - d.key"); console.log(d.key) + console.log("vis.selectAll - d.values"); console.log(d.values) + console.log("vis.selectAll - x(d.key)"); console.log(x(d.key)) + return x(d.key); + }) + .attr('width', x.rangeBand()) + .attr('y', function(d) { return y(d.values.length); }) + .attr('height', function(d) { return height - y(d.values.length); }) + .append('svg:title') + //.text(pathTitleFunc); + .text(function(d) { return d.key + ' (' + d.values.length + ')'; }); + + + vis.append('text') + .attr('x', width / 2) + .attr('y', height / 2) + .attr('class', barLabelClass) + .attr('text-anchor', 'middle') + .attr('dy', '.3em') + .text(barLabel); + + return serializeXmlNode(svg); + } + //Helper function function serializeXmlNode(xmlNode) { if (typeof window.XMLSerializer != "undefined") { diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css index 5ea17472..6a14b948 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts.css @@ -28,7 +28,7 @@ .clustermarker-cluster-pie g.arc{ fill-opacity: 0.5; } -.clustermarker-cluster-pie-label { +.clustermarker-cluster-pie-label, .clustermarker-cluster-bar-label { font-size: 14px; font-weight: bold; font-family: sans-serif; diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 677394d6..3bb30c3e 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -10,6 +10,7 @@ addClusterCharts( lat = NULL, layerId = NULL, group = NULL, + type = c("pie", "bar"), rmax = 30, size = c(18, 18), popup = NULL, From 52e2fb21a2aecc232206dea91bb65eff2c97ed93 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 11:05:09 +0200 Subject: [PATCH 04/26] add clusterchartOptions and example icons --- .gitignore | 1 + NAMESPACE | 1 + R/clusterCharts.R | 32 ++++- inst/examples/clusterCharts_app.R | 18 ++- inst/examples/www/icons/Icon 25.svg | 1 + inst/examples/www/icons/Icon 29.svg | 1 + inst/examples/www/icons/Icon 5.svg | 1 + inst/examples/www/icons/Icon 8.svg | 1 + .../lfx-clustercharts-bindings.js | 134 +++++++----------- man/addClusterCharts.Rd | 7 +- man/clusterchartOptions.Rd | 34 +++++ 11 files changed, 139 insertions(+), 92 deletions(-) create mode 100644 inst/examples/www/icons/Icon 25.svg create mode 100644 inst/examples/www/icons/Icon 29.svg create mode 100644 inst/examples/www/icons/Icon 5.svg create mode 100644 inst/examples/www/icons/Icon 8.svg create mode 100644 man/clusterchartOptions.Rd diff --git a/.gitignore b/.gitignore index 1c055fc8..2baac07b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ newfeatures/ node_modules/ docs/ bower_components/ +inst/doc diff --git a/NAMESPACE b/NAMESPACE index 9452ef74..2904cabb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -37,6 +37,7 @@ export(clearFuture) export(clearHexbin) export(clearHistory) export(closeSidebar) +export(clusterchartOptions) export(context_mapmenuItems) export(context_markermenuItems) export(context_menuItem) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 8b25d0fa..61e5e914 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -27,11 +27,12 @@ clusterchartsDependencies <- function() { #' @param rmax The maxClusterRadius #' @param categoryField in meters #' @param popupFields in meters +#' @param options clusterchartOptions #' @inheritParams leaflet::addCircleMarkers #' @export addClusterCharts <- function( map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar"), - rmax = 30, size = c(18, 18), + options = clusterchartOptions(), popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, clusterOptions = NULL, clusterId = NULL, categoryField, categoryMap, popupFields = NULL, popupLabels = NULL, @@ -52,8 +53,8 @@ addClusterCharts <- function( } ## CSS string ############# - # browser() css <- paste(apply(categoryMap, 1, generate_css), collapse = "\n") + size <- options$size if (length(size) == 1) size <- rep(size, 2) css <- paste0(css, "\n.clustermarker {", "width: ",size[1],"px; height: ",size[2],"px;", @@ -78,7 +79,8 @@ addClusterCharts <- function( points <- derivePoints(data, lng, lat, missing(lng), missing(lat), "addClusterCharts") leaflet::invokeMethod( - map, NULL, "addClusterCharts", geojson, layerId, group, type, rmax, size, + map, NULL, "addClusterCharts", geojson, layerId, group, type, + options, popup, popupOptions, safeLabel(label, data), labelOptions, clusterOptions, clusterId, categoryField, categoryMap, popupFields, popupLabels, @@ -87,6 +89,30 @@ addClusterCharts <- function( leaflet::expandLimits(points$lat, points$lng) } +#' clusterchartOptions +#' @description Adds options for clusterCharts +#' @param rmax The maxClusterRadius +#' @param size the size +#' @param width the width +#' @param height the height +#' @param strokeWidth the strokeWidth +#' @param pieMultiplier the pieMultiplier +#' @param innerRadius the innerRadius +#' @export +clusterchartOptions <- function(rmax = 30, size = c(20, 20), + width = 40, height = 50, + strokeWidth = 1, + pieMultiplier = 2, + innerRadius = -10) { + list( + rmax = rmax, + size = size, + width = width, + height = height, + strokeWidth = strokeWidth, + innerRadius = innerRadius + ) +} generate_css <- function(row) { label <- row["label"] diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index 9797f5e2..c3246825 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -23,6 +23,8 @@ ui <- fluidPage( ) ) +options("shiny.autoreload" = TRUE) + server <- function(input, output, session) { output$map <- renderLeaflet({ leaflet() %>% addMapPane("clusterpane", 420) %>% @@ -31,18 +33,20 @@ server <- function(input, output, session) { # addCircleMarkers(data = data, clusterOptions = markerClusterOptions()) %>% # addCircleMarkers(data = data, options = pathOptions(pane = "clusterpane")) %>% addClusterCharts(data = data - , rmax = 50 - , size = 50 + , options = clusterchartOptions(rmax = 40, size = 40, + width = 40, height = 40, + strokeWidth = 0.7, + innerRadius = 14) , type = "bar" , categoryField = "category" , categoryMap = data.frame(label = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), - # color = c("#F88", "#FA0", "#FF3", "#BFB"), + color = c("#F88", "#FA0", "#FF3", "#BFB"), + # color = c("lightblue", "orange", "lightyellow", "lightgreen"), + # color = c("cyan", "darkorange", "yellow", "#9fca8b"), + icons = c("icons/Icon 29.svg", "icons/Icon 8.svg", "icons/Icon 5.svg", "icons/Icon 25.svg"), # stroke = c("#800", "#B60", "#D80", "#070") - color = c("lightblue", "orange", "lightyellow", "lightgreen"), - icons = c("icons/Icon 1.svg", "icons/Icon 10.svg", "icons/Icon 20.svg", "icons/Icon 25.svg"), - # color = c("blue", "darkorange", "yellow", "green"), - stroke = "black" + stroke = "gray" ) , group = "clustermarkers" # group = "zipcode", diff --git a/inst/examples/www/icons/Icon 25.svg b/inst/examples/www/icons/Icon 25.svg new file mode 100644 index 00000000..71dc051e --- /dev/null +++ b/inst/examples/www/icons/Icon 25.svg @@ -0,0 +1 @@ +Asset 25 \ No newline at end of file diff --git a/inst/examples/www/icons/Icon 29.svg b/inst/examples/www/icons/Icon 29.svg new file mode 100644 index 00000000..ef47d421 --- /dev/null +++ b/inst/examples/www/icons/Icon 29.svg @@ -0,0 +1 @@ +Asset 29 \ No newline at end of file diff --git a/inst/examples/www/icons/Icon 5.svg b/inst/examples/www/icons/Icon 5.svg new file mode 100644 index 00000000..f9f2f474 --- /dev/null +++ b/inst/examples/www/icons/Icon 5.svg @@ -0,0 +1 @@ +Asset 5 \ No newline at end of file diff --git a/inst/examples/www/icons/Icon 8.svg b/inst/examples/www/icons/Icon 8.svg new file mode 100644 index 00000000..8c6018fc --- /dev/null +++ b/inst/examples/www/icons/Icon 8.svg @@ -0,0 +1 @@ +Asset 8 \ No newline at end of file diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 3cd08649..9af0465a 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -1,5 +1,6 @@ /* global LeafletWidget, $, L */ -LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, rmax, size, +LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, + options, popup, popupOptions, label, labelOptions, clusterOptions, clusterId, categoryField, categoryMap, popupFields, popupLabels, @@ -7,6 +8,10 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var map = this; + var rmax = options.rmax ? options.rmax : 30; + var innerRadius = options.innerRadius ? options.innerRadius : -10; + var strokeWidth = options.strokeWidth ? options.strokeWidth : 1; + var markerclusters = L.markerClusterGroup( Object.assign({ maxClusterRadius: 2 * rmax, @@ -101,60 +106,44 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, } function defineClusterIcon(cluster) { var children = cluster.getAllChildMarkers(), - n = children.length, //Get number of markers in cluster - strokeWidth = 1, //Set clusterpie stroke width - r = rmax-2*strokeWidth-(n<10?12:n<100?8:n<1000?4:0), //Calculate clusterpie radius... + n = children.length //Get number of markers in cluster + var r = rmax-2*strokeWidth-(n<10?12:n<100?8:n<1000?4:0), //Calculate clusterpie radius... iconDim = (r+strokeWidth)*2, //...and divIcon dimensions (leaflet really want to know the size) html; - //bake some svg markup - var data = d3.nest() //Build a dataset for the pie chart - .key(function(d) { return d.feature.properties[categoryField]; }) - .entries(children, d3.map) - - // Group data by categoryField - /* - var data = Array.from( - d3.group(children, d => d.feature.properties[categoryField]), - ([key, values]) => ({ key, values }) - ); - */ - - if (type == "pie") { - console.log("Piechart") - html = bakeThePie({ - data: data, - valueFunc: function(d){return d.values.length;}, - strokeWidth: strokeWidth, - outerRadius: r, - innerRadius: r-10, - pieClass: 'cluster-pie', - pieLabel: n, - pieLabelClass: 'clustermarker-cluster-pie-label', - pathClassFunc: function(d){ - return "category-" + d.data.key; - }, - pathTitleFunc: function(d){ - return d.data.key + ' (' + d.data.values.length + ')'; - } - }) - } else { - console.log("Barchart") - html = bakeTheBarChart({ - data: data, - valueFunc: function(d){return d.values.length;}, - barClass: 'cluster-bar', - barLabel: n, - barLabelClass: 'clustermarker-cluster-bar-label', - pathClassFunc: function(d){ - console.log("d.data"); console.log(d.data) - //return "category-" + d.data.key; - }, - pathTitleFunc: function(d){ - console.log("d.data"); console.log(d.data) - //return d.data.key + ' (' + d.data.values.length + ')'; - } - }); + + //bake some svg markup + var data = d3.nest() //Build a dataset for the pie chart + .key(function(d) { return d.feature.properties[categoryField]; }) + .entries(children, d3.map) + + if (type == "pie") { + console.log("Piechart") + html = bakeThePie({ + data: data, + valueFunc: function(d){return d.values.length;}, + outerRadius: r, + innerRadius: r-innerRadius, + pieClass: 'cluster-pie', + pieLabel: n, + pieLabelClass: 'clustermarker-cluster-pie-label', + pathClassFunc: function(d){ + return "category-" + d.data.key; + }, + pathTitleFunc: function(d){ + return d.data.key + ' (' + d.data.values.length + ')'; } + }) + } else { + console.log("Barchart") + html = bakeTheBarChart({ + data: data, + barClass: 'cluster-bar', + barLabel: n, + width: options.width ? options.width : 70, + height: options.height ? options.height : 40, + barLabelClass: 'clustermarker-cluster-bar-label' + }); + } //Create a new divIcon and assign the svg markup to the html property var myIcon = new L.DivIcon({ @@ -163,8 +152,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, iconSize: new L.Point(iconDim, iconDim) }); - console.log("r"); console.log(r) - //console.log("data"); console.log(data) return myIcon; } //function that generates a svg markup for the Pie chart @@ -173,16 +160,16 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (!options.data || !options.valueFunc) { return ''; } + console.log("bakeThePie with these options"); console.log(options) var data = options.data, valueFunc = options.valueFunc, - r = options.outerRadius?options.outerRadius:28, //Default outer radius = 28px - rInner = options.innerRadius?options.innerRadius:r-10, //Default inner radius = r-10 - strokeWidth = options.strokeWidth?options.strokeWidth:1, //Default stroke is 1 - pathClassFunc = options.pathClassFunc?options.pathClassFunc:function(){return '';}, //Class for each path - pathTitleFunc = options.pathTitleFunc?options.pathTitleFunc:function(){return '';}, //Title for each path - pieClass = options.pieClass?options.pieClass:'marker-cluster-pie', //Class for the whole pie + r = options.outerRadius, + rInner = options.innerRadius, + pathClassFunc = options.pathClassFunc, + pathTitleFunc = options.pathTitleFunc, + pieClass = options.pieClass, pieLabel = options.pieLabel?options.pieLabel:d3.sum(data,valueFunc), //Label for the whole pie - pieLabelClass = options.pieLabelClass?options.pieLabelClass:'marker-cluster-pie-label',//Class for the pie label + pieLabelClass = options.pieLabelClass, origo = (r+strokeWidth), //Center coordinate w = origo*2, //width and height of the svg element @@ -226,18 +213,16 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, //function that generates a svg markup for the Bar chart function bakeTheBarChart(options) { - if (!options.data || !options.valueFunc) { + if (!options.data) { return ''; } + console.log("bakeTheBarChart with these options"); console.log(options) var data = options.data, - valueFunc = options.valueFunc, - barClass = options.barClass ? options.barClass : 'marker-cluster-bar', + barClass = options.barClass, barLabel = options.barLabel ? options.barLabel : d3.sum(data, function(d) { return d.values.length; }), - barLabelClass = options.barLabelClass ? options.barLabelClass : 'marker-cluster-bar-label', - pathClassFunc = options.pathClassFunc?options.pathClassFunc:function(){return '';}, - pathTitleFunc = options.pathTitleFunc?options.pathTitleFunc:function(){return '';}, - width = 100, - height = 50, + barLabelClass = options.barLabelClass, + width = options.width, + height = options.height, x = d3.scale.ordinal().rangeRoundBands([0, width], 0.1), y = d3.scale.linear().range([height, 0]); @@ -253,31 +238,23 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, vis.selectAll('.bar') .data(data) .enter().append('rect') - //.attr('class', 'bar') .attr('class', function(d) { - console.log("category-"+d.key) return "category-"+d.key }) - //.attr('class', pathClassFunc) .attr('x', function(d) { - console.log("vis.selectAll - d.key"); console.log(d.key) - console.log("vis.selectAll - d.values"); console.log(d.values) - console.log("vis.selectAll - x(d.key)"); console.log(x(d.key)) return x(d.key); }) .attr('width', x.rangeBand()) .attr('y', function(d) { return y(d.values.length); }) .attr('height', function(d) { return height - y(d.values.length); }) .append('svg:title') - //.text(pathTitleFunc); .text(function(d) { return d.key + ' (' + d.values.length + ')'; }); - vis.append('text') .attr('x', width / 2) .attr('y', height / 2) .attr('class', barLabelClass) - .attr('text-anchor', 'middle') + .attr('text-anchor', 'top') .attr('dy', '.3em') .text(barLabel); @@ -313,7 +290,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, legendItems.enter() .append('div') .attr('class', function(d) { - console.log("d"); console.log(d) return 'category-' + d.value; }) .classed('legenditem', true) diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 3bb30c3e..3b8d3f67 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -11,8 +11,7 @@ addClusterCharts( layerId = NULL, group = NULL, type = c("pie", "bar"), - rmax = 30, - size = c(18, 18), + options = clusterchartOptions(), popup = NULL, popupOptions = NULL, label = NULL, @@ -49,7 +48,7 @@ Human-friendly group names are permitted--they need not be short, identifier-style names. Any number of layers and even different types of layers (e.g. markers and polygons) can share the same group name.} -\item{rmax}{The maxClusterRadius} +\item{options}{clusterchartOptions} \item{popup}{a character vector of the HTML content for the popups (you are recommended to escape the text using \code{\link[htmltools]{htmlEscape}()} @@ -80,6 +79,8 @@ initially, but can be overridden} \item{lat_center, lng_center}{lat/lng for the center} \item{radius}{in meters} + +\item{rmax}{The maxClusterRadius} } \description{ Adds a Great Circle to the map. diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd new file mode 100644 index 00000000..cb48eb9b --- /dev/null +++ b/man/clusterchartOptions.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/clusterCharts.R +\name{clusterchartOptions} +\alias{clusterchartOptions} +\title{clusterchartOptions} +\usage{ +clusterchartOptions( + rmax = 30, + size = c(20, 20), + width = 40, + height = 50, + strokeWidth = 1, + pieMultiplier = 2, + innerRadius = -10 +) +} +\arguments{ +\item{rmax}{The maxClusterRadius} + +\item{size}{the size} + +\item{width}{the width} + +\item{height}{the height} + +\item{strokeWidth}{the strokeWidth} + +\item{pieMultiplier}{the pieMultiplier} + +\item{innerRadius}{the innerRadius} +} +\description{ +Adds options for clusterCharts +} From ffb91fc91d7dcbc99afe74d99bfe90154f2fcab2 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 11:20:53 +0200 Subject: [PATCH 05/26] docs: descr --- R/clusterCharts.R | 29 +++++++++---------- inst/examples/clusterCharts_app.R | 6 ++-- .../lfx-clustercharts-bindings.js | 16 ++++++---- man/addClusterCharts.Rd | 24 +++++++-------- man/clusterchartOptions.Rd | 15 ++++------ 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 61e5e914..9aa6fff2 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -21,13 +21,14 @@ clusterchartsDependencies <- function() { #' addClusterCharts -#' @description Adds a Great Circle to the map. -#' @param lat_center,lng_center lat/lng for the center -#' @param radius in meters -#' @param rmax The maxClusterRadius -#' @param categoryField in meters -#' @param popupFields in meters -#' @param options clusterchartOptions +#' @description Adds cluster charts (either pie or bar charts) to a Leaflet map. +#' @param type The type of chart to use for clusters, either "pie" or "bar". +#' @param categoryField The name of the feature property used to categorize the charts. +#' @param categoryMap A data frame mapping categories to chart properties (label, color, icons, stroke). +#' @param popup Use the column name given in popup to collect the feature property with this name. +#' @param popupFields A string or vector of strings indicating the feature properties to include in popups. +#' @param popupLabels A string or vector of strings indicating the labels for the popup fields. +#' @param options Additional options for cluster charts (see \code{\link{clusterchartOptions}}). #' @inheritParams leaflet::addCircleMarkers #' @export addClusterCharts <- function( @@ -91,18 +92,16 @@ addClusterCharts <- function( #' clusterchartOptions #' @description Adds options for clusterCharts -#' @param rmax The maxClusterRadius -#' @param size the size -#' @param width the width -#' @param height the height -#' @param strokeWidth the strokeWidth -#' @param pieMultiplier the pieMultiplier -#' @param innerRadius the innerRadius +#' @param rmax The maximum radius of the clusters. +#' @param size The size of the cluster markers. +#' @param strokeWidth The stroke width in the chart. +#' @param width The width of the bar-chart. +#' @param height The height of the bar-chart. +#' @param innerRadius The inner radius of the pie-chart. #' @export clusterchartOptions <- function(rmax = 30, size = c(20, 20), width = 40, height = 50, strokeWidth = 1, - pieMultiplier = 2, innerRadius = -10) { list( rmax = rmax, diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index c3246825..a040b116 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -33,9 +33,9 @@ server <- function(input, output, session) { # addCircleMarkers(data = data, clusterOptions = markerClusterOptions()) %>% # addCircleMarkers(data = data, options = pathOptions(pane = "clusterpane")) %>% addClusterCharts(data = data - , options = clusterchartOptions(rmax = 40, size = 40, - width = 40, height = 40, - strokeWidth = 0.7, + , options = clusterchartOptions(rmax = 40, size = 30, + width = 30, height = 30, + strokeWidth = 10, innerRadius = 14) , type = "bar" , categoryField = "category" diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 9af0465a..83b7533f 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -141,7 +141,13 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, barLabel: n, width: options.width ? options.width : 70, height: options.height ? options.height : 40, - barLabelClass: 'clustermarker-cluster-bar-label' + barLabelClass: 'clustermarker-cluster-bar-label', + pathClassFunc: function(d){ + return "category-" + d.key; + }, + pathTitleFunc: function(d){ + return d.key + ' (' + d.values.length + ')'; + } }); } @@ -223,6 +229,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, barLabelClass = options.barLabelClass, width = options.width, height = options.height, + pathClassFunc = options.pathClassFunc, + pathTitleFunc = options.pathTitleFunc, x = d3.scale.ordinal().rangeRoundBands([0, width], 0.1), y = d3.scale.linear().range([height, 0]); @@ -238,9 +246,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, vis.selectAll('.bar') .data(data) .enter().append('rect') - .attr('class', function(d) { - return "category-"+d.key - }) + .attr('class', pathClassFunc) .attr('x', function(d) { return x(d.key); }) @@ -248,7 +254,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('y', function(d) { return y(d.values.length); }) .attr('height', function(d) { return height - y(d.values.length); }) .append('svg:title') - .text(function(d) { return d.key + ' (' + d.values.length + ')'; }); + .text(pathTitleFunc); vis.append('text') .attr('x', width / 2) diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 3b8d3f67..5ef618f0 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -48,11 +48,11 @@ Human-friendly group names are permitted--they need not be short, identifier-style names. Any number of layers and even different types of layers (e.g. markers and polygons) can share the same group name.} -\item{options}{clusterchartOptions} +\item{type}{The type of chart to use for clusters, either "pie" or "bar".} -\item{popup}{a character vector of the HTML content for the popups (you are -recommended to escape the text using \code{\link[htmltools]{htmlEscape}()} -for security reasons)} +\item{options}{Additional options for cluster charts (see \code{\link{clusterchartOptions}}).} + +\item{popup}{Use the column name given in popup to collect the feature property with this name.} \item{popupOptions}{A Vector of \code{\link[leaflet]{popupOptions}} to provide popups} @@ -68,20 +68,18 @@ options} \item{clusterId}{the id for the marker cluster layer} -\item{categoryField}{in meters} +\item{categoryField}{The name of the feature property used to categorize the charts.} + +\item{categoryMap}{A data frame mapping categories to chart properties (label, color, icons, stroke).} + +\item{popupFields}{A string or vector of strings indicating the feature properties to include in popups.} -\item{popupFields}{in meters} +\item{popupLabels}{A string or vector of strings indicating the labels for the popup fields.} \item{data}{the data object from which the argument values are derived; by default, it is the \code{data} object provided to \code{leaflet()} initially, but can be overridden} - -\item{lat_center, lng_center}{lat/lng for the center} - -\item{radius}{in meters} - -\item{rmax}{The maxClusterRadius} } \description{ -Adds a Great Circle to the map. +Adds cluster charts (either pie or bar charts) to a Leaflet map. } diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index cb48eb9b..ccf60b2c 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -10,24 +10,21 @@ clusterchartOptions( width = 40, height = 50, strokeWidth = 1, - pieMultiplier = 2, innerRadius = -10 ) } \arguments{ -\item{rmax}{The maxClusterRadius} +\item{rmax}{The maximum radius of the clusters.} -\item{size}{the size} +\item{size}{The size of the cluster markers.} -\item{width}{the width} +\item{width}{The width of the bar-chart.} -\item{height}{the height} +\item{height}{The height of the bar-chart.} -\item{strokeWidth}{the strokeWidth} +\item{strokeWidth}{The stroke width in the chart.} -\item{pieMultiplier}{the pieMultiplier} - -\item{innerRadius}{the innerRadius} +\item{innerRadius}{The inner radius of the pie-chart.} } \description{ Adds options for clusterCharts From a0f4a21f83e55307a8948cd63a9f1ef45d02fcc8 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 13:45:29 +0200 Subject: [PATCH 06/26] rm vignettes, add markercluster from leafletDeps, add examples, fix warnings, add categoryMap logic --- DESCRIPTION | 2 - R/clusterCharts.R | 94 +++++++++++++++---- inst/examples/clusterCharts_app.R | 31 +++--- .../www/icons/{Icon 25.svg => Icon25.svg} | 0 .../www/icons/{Icon 29.svg => Icon29.svg} | 0 .../www/icons/{Icon 5.svg => Icon5.svg} | 0 .../www/icons/{Icon 8.svg => Icon8.svg} | 0 .../lfx-clustercharts-bindings.js | 27 +++--- man/addClusterCharts.Rd | 46 +++++++++ man/clusterchartOptions.Rd | 2 +- 10 files changed, 150 insertions(+), 52 deletions(-) rename inst/examples/www/icons/{Icon 25.svg => Icon25.svg} (100%) rename inst/examples/www/icons/{Icon 29.svg => Icon29.svg} (100%) rename inst/examples/www/icons/{Icon 5.svg => Icon5.svg} (100%) rename inst/examples/www/icons/{Icon 8.svg => Icon8.svg} (100%) diff --git a/DESCRIPTION b/DESCRIPTION index 789e5076..dcbc541b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -29,9 +29,7 @@ Suggests: htmlwidgets, covr, curl, - knitr, rmarkdown URL: https://trafficonese.github.io/leaflet.extras2/, https://github.com/trafficonese/leaflet.extras2 BugReports: https://github.com/trafficonese/leaflet.extras2/issues RoxygenNote: 7.3.1 -VignetteBuilder: knitr diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 9aa6fff2..466f4455 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -7,11 +7,8 @@ clusterchartsDependencies <- function() { htmltools::htmlDependency( "lfx-clustercharts", version = "1.0.0", src = system.file("htmlwidgets/lfx-clustercharts", package = "leaflet.extras2"), - stylesheet = c("MarkerCluster.css", - "MarkerCluster.Default.css", - "lfx-clustercharts.css"), + stylesheet = c("lfx-clustercharts.css"), script = c( - "leaflet.markercluster.js", "d3.v3.min.js", "lfx-clustercharts-bindings.js" ) @@ -29,8 +26,51 @@ clusterchartsDependencies <- function() { #' @param popupFields A string or vector of strings indicating the feature properties to include in popups. #' @param popupLabels A string or vector of strings indicating the labels for the popup fields. #' @param options Additional options for cluster charts (see \code{\link{clusterchartOptions}}). +#' @param legendOptions A list of options for the legend, including the title and position. +#' @param markerOptions Additional options for markers (see \code{\link[leaflet:markerOptions]{markerOptions::markerOptions()}}). #' @inheritParams leaflet::addCircleMarkers #' @export +#' @examples +#' # Example usage: +#' library(sf) +#' library(geojsonsf) +#' library(leaflet) +#' library(leaflet.extras2) +#' +#' data <- sf::st_as_sf(breweries91) +#' categories <- c("Schwer", "Mäßig", "Leicht", "kein Schaden") +#' data$category <- sample(categories, size = nrow(data), replace = TRUE) +#' +#' ## Pie Chart +#' leaflet() %>% +#' addProviderTiles("CartoDB.Positron") %>% +#' leaflet::addLayersControl(overlayGroups = "clustermarkers") %>% +#' addClusterCharts(data = data +#' , categoryField = "category" +#' , categoryMap = data.frame(labels = categories, +#' colors = c("#F88", "#FA0", "#FF3", "#BFB"), +#' strokes = "gray") +#' , group = "clustermarkers" +#' , popupFields = c("brewery", "address", "zipcode", "category") +#' , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") +#' , label = "brewery" +#' ) +#' +#' ## Bar Chart +#' leaflet() %>% +#' addProviderTiles("CartoDB.Positron") %>% +#' leaflet::addLayersControl(overlayGroups = "clustermarkers") %>% +#' addClusterCharts(data = data +#' , type = "bar" +#' , categoryField = "category" +#' , categoryMap = data.frame(labels = categories, +#' colors = c("#F88", "#FA0", "#FF3", "#BFB"), +#' strokes = "gray") +#' , group = "clustermarkers" +#' , popupFields = c("brewery", "address", "zipcode", "category") +#' , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") +#' , label = "brewery" +#' ) addClusterCharts <- function( map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar"), options = clusterchartOptions(), @@ -41,10 +81,25 @@ addClusterCharts <- function( position = "topright"), data = getMapData(map)) { - ## Check type, labelOptions, popupFields, popupLabels, clusterOptions ############ + ## Check arguments ############ type <- match.arg(type) - if (missing(labelOptions)) - labelOptions <- labelOptions() + if (missing(labelOptions)) labelOptions <- labelOptions() + if (missing(categoryMap)) { + stop("The `categoryMap` is missing.\n", + "A `categoryMap` is required to associate `labels`, `colors`, `icons`, and `strokes` with individual features\n", + "based on the specified `categoryField`: ", categoryField, ".") + } + if (is.null(categoryMap$labels)) { + warning("The `categoryMap` is missing a `labels` column.\n", + "Values will be generated based on the unique values of `", categoryField, "` in `data`.\n", + "Note: The order may be incorrect, so it is recommended to add a correct `labels` column in the `categoryMap`.") + categoryMap$labels <- unique(data[[categoryField]]) + } + if (is.null(categoryMap$colors)) { + warning("The `categoryMap` is missing a `color` column.\n", + "An automatic color palette will be assigned.") + categoryMap$colors <- colorRampPalette(c("#fc8d8d", "white", "lightblue"))(nrow(categoryMap)) + } if (!is.null(popupFields) && is.null(popupLabels)) { popupLabels <- popupFields } @@ -68,12 +123,17 @@ addClusterCharts <- function( src = system.file("htmlwidgets/lfx-clustercharts", package = "leaflet.extras2") ) ) - categoryMap <- setNames(as.list(categoryMap$label), seq.int(as.list(categoryMap$label))) + categoryMapList <- as.list(categoryMap$labels) + names(categoryMapList) <- seq.int(categoryMapList) ## Add Deps ############ - map$dependencies <- c(map$dependencies, csssrc, clusterchartsDependencies()) + map$dependencies <- c(map$dependencies, + csssrc, + leaflet::leafletDependencies$markerCluster(), + clusterchartsDependencies()) ## Make Geojson ########### + ##TODO - check if its a Point SimpleFeature geojson <- geojsonsf::sf_geojson(data) ## Derive Points and Invoke Method ################## @@ -84,7 +144,7 @@ addClusterCharts <- function( options, popup, popupOptions, safeLabel(label, data), labelOptions, clusterOptions, clusterId, - categoryField, categoryMap, popupFields, popupLabels, + categoryField, categoryMapList, popupFields, popupLabels, markerOptions, legendOptions ) %>% leaflet::expandLimits(points$lat, points$lng) @@ -102,7 +162,7 @@ addClusterCharts <- function( clusterchartOptions <- function(rmax = 30, size = c(20, 20), width = 40, height = 50, strokeWidth = 1, - innerRadius = -10) { + innerRadius = 10) { list( rmax = rmax, size = size, @@ -114,10 +174,10 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), } generate_css <- function(row) { - label <- row["label"] - color <- row["color"] - stroke <- row["stroke"] - icons <- row['icons'] + label <- row["labels"] + color <- row["colors"] + stroke <- row["strokes"] + icon <- row['icons'] if (is.null(color)) color <- stroke if (is.null(stroke)) stroke <- color @@ -132,10 +192,10 @@ generate_css <- function(row) { " border-color: ", stroke, ";\n", "}\n" ) - if (!is.null(icons)) { + if (!is.null(icon)) { css <- paste0(css, ".icon-", gsub(" ", ".", label), " {\n", - " background-image: url('", icons, "') !important;\n", + " background-image: url('", icon, "') !important;\n", " background-repeat: no-repeat !important;\n", " background-position: 0px 1px !important;\n", "}" diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index a040b116..7168b15a 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -1,19 +1,16 @@ library(shiny) library(sf) -library(geojsonsf) library(leaflet) library(leaflet.extras2) +options("shiny.autoreload" = TRUE) data <- sf::st_as_sf(breweries91) -data$category <- sample(factor(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), ordered = TRUE), size = nrow(data), replace = TRUE) +data$category <- sample(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), size = nrow(data), replace = TRUE) data$label <- paste0(data$brewery, "
", data$address) data$id <- paste0("ID", seq.int(nrow(data))) data$popup <- paste0("
", data$brewery, "
", data$address, "
") -# data <- geojson_sf("https://gist.githubusercontent.com/gisminister/10001728/raw/97156c7676f85a1f2689ce0adceec3a759baa359/traffic_accidents.geojson") -# data <- "https://gist.githubusercontent.com/gisminister/10001728/raw/97156c7676f85a1f2689ce0adceec3a759baa359/traffic_accidents.geojson" ui <- fluidPage( - # tags$head(tags$style(css)), leafletOutput("map", height = 500), splitLayout(cellWidths = paste0(rep(20,4), "%"), div(verbatimTextOutput("click")), @@ -23,30 +20,27 @@ ui <- fluidPage( ) ) -options("shiny.autoreload" = TRUE) - server <- function(input, output, session) { output$map <- renderLeaflet({ leaflet() %>% addMapPane("clusterpane", 420) %>% addProviderTiles("CartoDB") %>% leaflet::addLayersControl(overlayGroups = "clustermarkers") %>% # addCircleMarkers(data = data, clusterOptions = markerClusterOptions()) %>% - # addCircleMarkers(data = data, options = pathOptions(pane = "clusterpane")) %>% addClusterCharts(data = data , options = clusterchartOptions(rmax = 40, size = 30, width = 30, height = 30, - strokeWidth = 10, - innerRadius = 14) - , type = "bar" + strokeWidth = 1, + innerRadius = 10) + # , type = "bar" , categoryField = "category" , categoryMap = - data.frame(label = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), - color = c("#F88", "#FA0", "#FF3", "#BFB"), - # color = c("lightblue", "orange", "lightyellow", "lightgreen"), - # color = c("cyan", "darkorange", "yellow", "#9fca8b"), - icons = c("icons/Icon 29.svg", "icons/Icon 8.svg", "icons/Icon 5.svg", "icons/Icon 25.svg"), - # stroke = c("#800", "#B60", "#D80", "#070") - stroke = "gray" + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("#F88", "#FA0", "#FF3", "#BFB"), + # colors = c("lightblue", "orange", "lightyellow", "lightgreen"), + # colors = c("cyan", "darkorange", "yellow", "#9fca8b"), + icons = c("icons/Icon29.svg", "icons/Icon8.svg", "icons/Icon5.svg", "icons/Icon25.svg"), + # strokes = c("#800", "#B60", "#D80", "#070") + strokes = "gray" ) , group = "clustermarkers" # group = "zipcode", @@ -61,7 +55,6 @@ server <- function(input, output, session) { draggable = TRUE, keyboard = TRUE, title = "Some Marker Title", - alt = "The alt info", zIndexOffset = 100, opacity = 1, riseOnHover = TRUE, diff --git a/inst/examples/www/icons/Icon 25.svg b/inst/examples/www/icons/Icon25.svg similarity index 100% rename from inst/examples/www/icons/Icon 25.svg rename to inst/examples/www/icons/Icon25.svg diff --git a/inst/examples/www/icons/Icon 29.svg b/inst/examples/www/icons/Icon29.svg similarity index 100% rename from inst/examples/www/icons/Icon 29.svg rename to inst/examples/www/icons/Icon29.svg diff --git a/inst/examples/www/icons/Icon 5.svg b/inst/examples/www/icons/Icon5.svg similarity index 100% rename from inst/examples/www/icons/Icon 5.svg rename to inst/examples/www/icons/Icon5.svg diff --git a/inst/examples/www/icons/Icon 8.svg b/inst/examples/www/icons/Icon8.svg similarity index 100% rename from inst/examples/www/icons/Icon 8.svg rename to inst/examples/www/icons/Icon8.svg diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 83b7533f..298a836b 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -8,21 +8,19 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var map = this; + // options var rmax = options.rmax ? options.rmax : 30; - var innerRadius = options.innerRadius ? options.innerRadius : -10; var strokeWidth = options.strokeWidth ? options.strokeWidth : 1; + // Make L.markerClusterGroup, markers, fitBounds and renderLegend + console.log("geojson"); console.log(geojson) var markerclusters = L.markerClusterGroup( Object.assign({ maxClusterRadius: 2 * rmax, iconCreateFunction: defineClusterIcon // this is where the magic happens }, clusterOptions) ) - - map.addLayer(markerclusters); map.layerManager.addLayer(markerclusters, "cluster", clusterId, group); - - console.log("geojson"); console.log(geojson) var markers = L.geoJson(geojson, { pointToLayer: defineFeature, onEachFeature: defineFeaturePopup @@ -31,6 +29,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, map.fitBounds(markers.getBounds()); renderLegend(); + // Show/Hide the legend when the group is shown/hidden map.on('overlayadd', function(eventlayer){ if (eventlayer.name == group) { $(".clusterlegend").show() @@ -42,6 +41,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, } }); + // Functions function defineFeature(feature, latlng) { var categoryVal = feature.properties[categoryField] var myClass = 'clustermarker category-'+categoryVal+' icon-'+categoryVal; @@ -87,7 +87,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, popupContent = ''; if (popup && props[popup]) { popupContent = props[popup]; - } else { + } else if (popupFields !== null ) { popupContent += ''; popupFields.map( function(key, idx) { if (props[key]) { @@ -98,10 +98,13 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, }); popupContent += '
'; } - if (popupOptions !== null){ - layer.bindPopup(popupContent, popupOptions); - } else { - layer.bindPopup(popupContent); + + if (popupContent !== '') { + if (popupOptions !== null){ + layer.bindPopup(popupContent, popupOptions); + } else { + layer.bindPopup(popupContent); + } } } function defineClusterIcon(cluster) { @@ -118,6 +121,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (type == "pie") { console.log("Piechart") + var innerRadius = options.innerRadius ? options.innerRadius : -10; html = bakeThePie({ data: data, valueFunc: function(d){return d.values.length;}, @@ -216,7 +220,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, //Return the svg-markup rather than the actual element return serializeXmlNode(svg); } - //function that generates a svg markup for the Bar chart function bakeTheBarChart(options) { if (!options.data) { @@ -266,7 +269,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, return serializeXmlNode(svg); } - //Helper function function serializeXmlNode(xmlNode) { if (typeof window.XMLSerializer != "undefined") { @@ -276,7 +278,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, } return ""; } - //Function for generating a legend with the same categories as in the clusterPie function renderLegend() { var data = Object.entries(categoryMap).map(([key, value]) => ({ diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 5ef618f0..8e1595c1 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -76,6 +76,10 @@ options} \item{popupLabels}{A string or vector of strings indicating the labels for the popup fields.} +\item{markerOptions}{Additional options for markers (see \code{\link[leaflet:markerOptions]{markerOptions::markerOptions()}}).} + +\item{legendOptions}{A list of options for the legend, including the title and position.} + \item{data}{the data object from which the argument values are derived; by default, it is the \code{data} object provided to \code{leaflet()} initially, but can be overridden} @@ -83,3 +87,45 @@ initially, but can be overridden} \description{ Adds cluster charts (either pie or bar charts) to a Leaflet map. } +\examples{ +# Example usage: +library(sf) +library(geojsonsf) +library(leaflet) +library(leaflet.extras2) + +data <- sf::st_as_sf(breweries91) +categories <- c("Schwer", "Mäßig", "Leicht", "kein Schaden") +data$category <- sample(categories, size = nrow(data), replace = TRUE) + +## Pie Chart +leaflet() \%>\% + addProviderTiles("CartoDB.Positron") \%>\% + leaflet::addLayersControl(overlayGroups = "clustermarkers") \%>\% + addClusterCharts(data = data + , categoryField = "category" + , categoryMap = data.frame(labels = categories, + colors = c("#F88", "#FA0", "#FF3", "#BFB"), + strokes = "gray") + , group = "clustermarkers" + , popupFields = c("brewery", "address", "zipcode", "category") + , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") + , label = "brewery" + ) + +## Bar Chart +leaflet() \%>\% + addProviderTiles("CartoDB.Positron") \%>\% + leaflet::addLayersControl(overlayGroups = "clustermarkers") \%>\% + addClusterCharts(data = data + , type = "bar" + , categoryField = "category" + , categoryMap = data.frame(labels = categories, + colors = c("#F88", "#FA0", "#FF3", "#BFB"), + strokes = "gray") + , group = "clustermarkers" + , popupFields = c("brewery", "address", "zipcode", "category") + , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") + , label = "brewery" + ) +} diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index ccf60b2c..d2b14490 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -10,7 +10,7 @@ clusterchartOptions( width = 40, height = 50, strokeWidth = 1, - innerRadius = -10 + innerRadius = 10 ) } \arguments{ From 1ce8be03e2417830d3f42daf8daf1d763dd87faf Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 15:03:16 +0200 Subject: [PATCH 07/26] check if sf, add more clusterchartOptions , add allTitles to label and background-rect --- R/clusterCharts.R | 31 +++++++--- inst/examples/clusterCharts_app.R | 7 ++- .../lfx-clustercharts-bindings.js | 62 ++++++++++++++++--- man/clusterchartOptions.Rd | 7 ++- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 466f4455..23c1e509 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -134,6 +134,9 @@ addClusterCharts <- function( ## Make Geojson ########### ##TODO - check if its a Point SimpleFeature + if (!inherits(sf::st_as_sf(data), "sf")) { + data <- sf::st_as_sf(data) + } geojson <- geojsonsf::sf_geojson(data) ## Derive Points and Invoke Method ################## @@ -162,15 +165,25 @@ addClusterCharts <- function( clusterchartOptions <- function(rmax = 30, size = c(20, 20), width = 40, height = 50, strokeWidth = 1, - innerRadius = 10) { - list( - rmax = rmax, - size = size, - width = width, - height = height, - strokeWidth = strokeWidth, - innerRadius = innerRadius - ) + innerRadius = 10, + labelBackground = FALSE, + labelFill = "white", + labelStroke = "black", + labelColor = "black", + labelOpacity = 0.9) { + filterNULL(list( + rmax = rmax + , size = size + , width = width + , height = height + , strokeWidth = strokeWidth + , innerRadius = innerRadius + , labelBackground = labelBackground + , labelFill = labelFill + , labelStroke = labelStroke + , labelColor = labelColor + , labelOpacity = labelOpacity + )) } generate_css <- function(row) { diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index 7168b15a..5722b0c2 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -29,7 +29,12 @@ server <- function(input, output, session) { addClusterCharts(data = data , options = clusterchartOptions(rmax = 40, size = 30, width = 30, height = 30, - strokeWidth = 1, + strokeWidth = 0.5, + labelBackground = T, + labelFill = "orange", + labelStroke = "gray10", + labelColor = "blue", + labelOpacity = 0.5, innerRadius = 10) # , type = "bar" , categoryField = "category" diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 298a836b..2e0ebd64 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -11,6 +11,11 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // options var rmax = options.rmax ? options.rmax : 30; var strokeWidth = options.strokeWidth ? options.strokeWidth : 1; + var labelBackground = options.labelBackground; + var labelFill = options.labelFill ? options.labelFill : "white"; + var labelStroke = options.labelStroke ? options.labelStroke : "black"; + var labelColor = options.labelColor ? options.labelColor : "black"; + var labelOpacity = options.labelOpacity ? options.labelOpacity : 0.9; // Make L.markerClusterGroup, markers, fitBounds and renderLegend console.log("geojson"); console.log(geojson) @@ -196,6 +201,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('width', w) .attr('height', h); + // Arcs var arcs = vis.selectAll('g.arc') .data(donut.value(valueFunc)) .enter().append('svg:g') @@ -209,13 +215,34 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .append('svg:title') .text(pathTitleFunc); + // Bar Label Background + console.log("d3pie data") + pathTitleFunc = function(d){ + return d.key + ' (' + d.values.length + ')'; + } + var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); + if (labelBackground && labelBackground == true) { + vis.append('circle') + .attr('r', rInner-5) + .attr('transform', 'translate(' + origo + ',' + origo + ')') + .attr('fill', labelFill) + .attr('stroke', labelStroke) + .attr('stroke-width', strokeWidth) + .attr('opacity', labelOpacity) + .append('svg:title') + .text(allTitles); // Title for the rectangle with all values + } + // Text vis.append('text') .attr('x',origo) .attr('y',origo) .attr('class', pieLabelClass) .attr('text-anchor', 'middle') + .attr('fill', labelColor) .attr('dy','.3em') - .text(pieLabel); + .text(pieLabel) + .append('svg:title') + .text(allTitles); //Return the svg-markup rather than the actual element return serializeXmlNode(svg); @@ -244,28 +271,47 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var vis = d3.select(svg) .attr('class', barClass) .attr('width', width) - .attr('height', height); + .attr('height', height + 20); + // Bars vis.selectAll('.bar') .data(data) .enter().append('rect') .attr('class', pathClassFunc) - .attr('x', function(d) { - return x(d.key); - }) + .attr('x', function(d) { return x(d.key); }) .attr('width', x.rangeBand()) .attr('y', function(d) { return y(d.values.length); }) .attr('height', function(d) { return height - y(d.values.length); }) .append('svg:title') .text(pathTitleFunc); + // Bar Label Background + var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); + if (labelBackground && labelBackground == true) { + vis.append('rect') + .attr('x', 0) // Adjust the width of the background + .attr('y', 35) // Adjust the y position for the background + .attr('width', 30) // Width of the background + .attr('height', 15) // Height of the background + .attr('fill', labelFill) + .attr('stroke', labelStroke) + .attr('opacity', labelOpacity) + .attr('stroke-width', strokeWidth) + .append('svg:title') + .text(allTitles); // Title for the rectangle with all values + } + + // Bar Label vis.append('text') .attr('x', width / 2) - .attr('y', height / 2) + .attr('y', 43) // Adjust the y position for the text .attr('class', barLabelClass) - .attr('text-anchor', 'top') + .attr('text-anchor', 'middle') .attr('dy', '.3em') - .text(barLabel); + .attr('fill', labelColor) + .text(barLabel) + .append('svg:title') + .text(allTitles); // Title for the rectangle with all values return serializeXmlNode(svg); } diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index d2b14490..676f86f9 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -10,7 +10,12 @@ clusterchartOptions( width = 40, height = 50, strokeWidth = 1, - innerRadius = 10 + innerRadius = 10, + labelBackground = FALSE, + labelFill = "white", + labelStroke = "black", + labelColor = "black", + labelOpacity = 0.9 ) } \arguments{ From ac809d6086e5f812d61b7909e832c7c2518f827a Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sun, 4 Aug 2024 23:48:51 +0200 Subject: [PATCH 08/26] feat: horizontal barchart --- R/clusterCharts.R | 3 +- .../lfx-clustercharts-bindings.js | 86 +++++++++++++++++++ man/addClusterCharts.Rd | 2 +- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 23c1e509..c7bb2545 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -72,7 +72,7 @@ clusterchartsDependencies <- function() { #' , label = "brewery" #' ) addClusterCharts <- function( - map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar"), + map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal"), options = clusterchartOptions(), popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, clusterOptions = NULL, clusterId = NULL, @@ -133,7 +133,6 @@ addClusterCharts <- function( clusterchartsDependencies()) ## Make Geojson ########### - ##TODO - check if its a Point SimpleFeature if (!inherits(sf::st_as_sf(data), "sf")) { data <- sf::st_as_sf(data) } diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 2e0ebd64..50d3e412 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -142,6 +142,22 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, return d.data.key + ' (' + d.data.values.length + ')'; } }) + } else if (type == "horizontal") { + console.log("Barchart horizontal") + html = bakeTheBarChartHorizontal({ + data: data, + barClass: 'cluster-bar', + barLabel: n, + width: options.width ? options.width : 70, + height: options.height ? options.height : 40, + barLabelClass: 'clustermarker-cluster-bar-label', + pathClassFunc: function(d){ + return "category-" + d.key; + }, + pathTitleFunc: function(d){ + return d.key + ' (' + d.values.length + ')'; + } + }); } else { console.log("Barchart") html = bakeTheBarChart({ @@ -315,6 +331,76 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, return serializeXmlNode(svg); } + //function that generates a svg markup for the horizontal Bar chart + function bakeTheBarChartHorizontal(options) { + if (!options.data) { + return ''; + } + console.log("bakeTheBarChart with these options"); console.log(options) + var data = options.data, + barClass = options.barClass, + barLabel = options.barLabel ? options.barLabel : d3.sum(data, function(d) { return d.values.length; }), + barLabelClass = options.barLabelClass, + width = options.width, + height = options.height, + pathClassFunc = options.pathClassFunc, + pathTitleFunc = options.pathTitleFunc, + x = d3.scale.linear().range([0, width]), // Linear scale for horizontal length + y = d3.scale.ordinal().rangeRoundBands([0, height], 0.1); // Ordinal scale for vertical positioning + + x.domain([0, d3.max(data, function(d) { return d.values.length; })]); + y.domain(data.map(function(d) { return d.key; })); + + var svg = document.createElementNS(d3.ns.prefix.svg, "svg"); + var vis = d3.select(svg) + .attr('class', barClass) + .attr('width', width) + .attr('height', height + 20); + + // Bars + vis.selectAll('.bar') + .data(data) + .enter().append('rect') + .attr('class', pathClassFunc) + .attr('x', 0) + .attr('y', function(d) { return y(d.key); }) + .attr('width', function(d) { return x(d.values.length); }) // Bar length based on x scale + .attr('height', y.rangeBand()) // Bar thickness based on y scale + .append('svg:title') + .text(pathTitleFunc); + + // Bar Label Background + var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); + if (labelBackground && labelBackground == true) { + vis.append('rect') + .attr('x', 0) // Adjust the width of the background + .attr('y', height) // Adjust the y position for the background + .attr('width', width) // Width of the background + .attr('height', 15) // Height of the background + .attr('fill', labelFill) + .attr('stroke', labelStroke) + .attr('opacity', labelOpacity) + .attr('stroke-width', strokeWidth) + .append('svg:title') + .text(allTitles); // Title for the rectangle with all values + } + + // Bar Label + vis.append('text') + .attr('x', width / 2) + .attr('y', (height + 5)) // Adjust the y position for the text + .attr('class', barLabelClass) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .attr('alignment-baseline', 'middle') + .attr('dy', '.3em') + .attr('fill', labelColor) + .text(barLabel) + .append('svg:title') + .text(allTitles); // Title for the rectangle with all values + + return serializeXmlNode(svg); + } //Helper function function serializeXmlNode(xmlNode) { if (typeof window.XMLSerializer != "undefined") { diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 8e1595c1..c8d185da 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -10,7 +10,7 @@ addClusterCharts( lat = NULL, layerId = NULL, group = NULL, - type = c("pie", "bar"), + type = c("pie", "bar", "horizontal"), options = clusterchartOptions(), popup = NULL, popupOptions = NULL, From d42ad28c0fe7c9ed7afc2b0db1d3ab55e984dca1 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Mon, 5 Aug 2024 10:44:47 +0200 Subject: [PATCH 09/26] switch to yyjsonr, cleanup markercluster deps --- DESCRIPTION | 2 +- NEWS.md | 3 + R/clusterCharts.R | 5 +- R/heightgraph.R | 10 ++-- R/timeslider.R | 10 ++-- inst/examples/clusterCharts_app.R | 3 +- inst/examples/timeslider_app.R | 1 - .../MarkerCluster.Default.css | 60 ------------------- .../lfx-clustercharts/MarkerCluster.css | 14 ----- .../leaflet.markercluster.js | 3 - man/addClusterCharts.Rd | 1 - man/addTimeslider.Rd | 1 - tests/testthat/test-timeslider.R | 3 - 13 files changed, 19 insertions(+), 97 deletions(-) delete mode 100644 inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css delete mode 100644 inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css delete mode 100644 inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js diff --git a/DESCRIPTION b/DESCRIPTION index dcbc541b..dc781a3c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,7 +22,7 @@ Suggests: jsonlite, shiny, sf, - geojsonsf, + yyjsonr, sp, testthat (>= 2.1.0), fontawesome, diff --git a/NEWS.md b/NEWS.md index 61a338f9..102bed15 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,9 @@ * Fix for roxygen2 > 7.0.0. #1491 * The opened sidebar tab is returned as Shiny input using the `sidebar_tabs` ID. * allow `...` in `antpathOptions` to be able to set the pane (e.g.: `renderer= JS('L.svg({pane: "my-pane"})')`) +* Added custom `clusterCharts` using `Leaflet.markercluster` and `d3` for piechart and barcharts. +* Switched from `geojsonsf` to `yyjsonr` (*heightgraph*, *timeslider*, *clustercharts*) + # leaflet.extras2 1.2.2 diff --git a/R/clusterCharts.R b/R/clusterCharts.R index c7bb2545..12d1d790 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -16,7 +16,6 @@ clusterchartsDependencies <- function() { ) } - #' addClusterCharts #' @description Adds cluster charts (either pie or bar charts) to a Leaflet map. #' @param type The type of chart to use for clusters, either "pie" or "bar". @@ -33,7 +32,6 @@ clusterchartsDependencies <- function() { #' @examples #' # Example usage: #' library(sf) -#' library(geojsonsf) #' library(leaflet) #' library(leaflet.extras2) #' @@ -136,7 +134,8 @@ addClusterCharts <- function( if (!inherits(sf::st_as_sf(data), "sf")) { data <- sf::st_as_sf(data) } - geojson <- geojsonsf::sf_geojson(data) + geojson <- yyjsonr::write_geojson_str(data) + class(geojson) <- c("geojson","json") ## Derive Points and Invoke Method ################## points <- derivePoints(data, lng, lat, missing(lng), missing(lat), diff --git a/R/heightgraph.R b/R/heightgraph.R index c37bee82..1ebe8d86 100644 --- a/R/heightgraph.R +++ b/R/heightgraph.R @@ -74,9 +74,9 @@ addHeightgraph <- function( pathOpts = leaflet::pathOptions(), options = heightgraphOptions()) { - if (!requireNamespace("geojsonsf")) { - stop("The package `geojsonsf` is needed for this plugin. ", - "Please install it with:\ninstall.packages('geojsonsf')") + if (!requireNamespace("yyjsonr")) { + stop("The package `yyjsonr` is needed for this plugin. ", + "Please install it with:\ninstall.packages('yyjsonr')") } ## TODO - Use all columns if NULL ?? @@ -94,7 +94,9 @@ addHeightgraph <- function( ## Change columnnames to `attributeType` and transform to Geojson data <- lapply(columns, function(x) { names(data)[names(data) == x] <- 'attributeType' - geojsonsf::sf_geojson(data) + data <- yyjsonr::write_geojson_str(data) + class(data) <- c("geojson","json") + data }) # Check if Properties and Data have same length diff --git a/R/timeslider.R b/R/timeslider.R index 52373c47..7cae497d 100644 --- a/R/timeslider.R +++ b/R/timeslider.R @@ -33,7 +33,6 @@ timesliderDependencies <- function() { #' library(leaflet) #' library(leaflet.extras2) #' library(sf) -#' library(geojsonsf) #' #' data <- sf::st_as_sf(leaflet::atlStorms2005[1,]) #' data <- st_cast(data, "POINT") @@ -92,11 +91,12 @@ addTimeslider <- function(map, data, radius = 10, bbox <- sf::st_bbox(data) ## Make GeoJSON - if (!requireNamespace("geojsonsf")) { - stop("The package `geojsonsf` is needed for this plugin. ", - "Please install it with:\ninstall.packages('geojsonsf')") + if (!requireNamespace("yyjsonr")) { + stop("The package `yyjsonr` is needed for this plugin. ", + "Please install it with:\ninstall.packages('yyjsonr')") } - data <- geojsonsf::sf_geojson(data) + data <- yyjsonr::write_geojson_str(data) + class(data) <- c("geojson","json") ## Add Deps and invoke Leaflet map$dependencies <- c(map$dependencies, timesliderDependencies()) diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index 5722b0c2..f730bd4c 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -29,7 +29,7 @@ server <- function(input, output, session) { addClusterCharts(data = data , options = clusterchartOptions(rmax = 40, size = 30, width = 30, height = 30, - strokeWidth = 0.5, + strokeWidth = 2, labelBackground = T, labelFill = "orange", labelStroke = "gray10", @@ -37,6 +37,7 @@ server <- function(input, output, session) { labelOpacity = 0.5, innerRadius = 10) # , type = "bar" + , type = "horizontal" , categoryField = "category" , categoryMap = data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), diff --git a/inst/examples/timeslider_app.R b/inst/examples/timeslider_app.R index 51513cf5..8cef7338 100644 --- a/inst/examples/timeslider_app.R +++ b/inst/examples/timeslider_app.R @@ -1,7 +1,6 @@ library(leaflet) library(leaflet.extras2) library(sf) -library(geojsonsf) library(shiny) data <- sf::st_as_sf(leaflet::atlStorms2005[1,]) diff --git a/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css b/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css deleted file mode 100644 index 6eb5a21b..00000000 --- a/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.Default.css +++ /dev/null @@ -1,60 +0,0 @@ -.marker-cluster-small { - background-color: rgba(181, 226, 140, 0.6); -} -.marker-cluster-small div { - background-color: rgba(110, 204, 57, 0.6); -} - -.marker-cluster-medium { - background-color: rgba(241, 211, 87, 0.6); -} -.marker-cluster-medium div { - background-color: rgba(240, 194, 12, 0.6); -} - -.marker-cluster-large { - background-color: rgba(253, 156, 115, 0.6); -} -.marker-cluster-large div { - background-color: rgba(241, 128, 23, 0.6); -} - -/* IE 6-8 fallback colors */ - .leaflet-oldie .marker-cluster-small { - background-color: rgb(181, 226, 140); - } -.leaflet-oldie .marker-cluster-small div { - background-color: rgb(110, 204, 57); -} - -.leaflet-oldie .marker-cluster-medium { - background-color: rgb(241, 211, 87); -} -.leaflet-oldie .marker-cluster-medium div { - background-color: rgb(240, 194, 12); -} - -.leaflet-oldie .marker-cluster-large { - background-color: rgb(253, 156, 115); -} -.leaflet-oldie .marker-cluster-large div { - background-color: rgb(241, 128, 23); -} - -.marker-cluster { - background-clip: padding-box; - border-radius: 20px; -} -.marker-cluster div { - width: 30px; - height: 30px; - margin-left: 5px; - margin-top: 5px; - - text-align: center; - border-radius: 15px; - font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; -} -.marker-cluster span { - line-height: 30px; -} diff --git a/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css b/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css deleted file mode 100644 index c60d71b7..00000000 --- a/inst/htmlwidgets/lfx-clustercharts/MarkerCluster.css +++ /dev/null @@ -1,14 +0,0 @@ -.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { - -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; - -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; - -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; - transition: transform 0.3s ease-out, opacity 0.3s ease-in; -} - -.leaflet-cluster-spider-leg { - /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ - -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; - -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; - -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; - transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; -} diff --git a/inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js b/inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js deleted file mode 100644 index fa90f6e5..00000000 --- a/inst/htmlwidgets/lfx-clustercharts/leaflet.markercluster.js +++ /dev/null @@ -1,3 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e.Leaflet=e.Leaflet||{},e.Leaflet.markercluster=e.Leaflet.markercluster||{}))}(this,function(e){"use strict";var t=L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,clusterPane:L.Marker.prototype.options.pane,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,i=this._zoom;if(e.__parent)for(;t.__parent._zoom>=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):e.getLatLng?this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e}),this):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e}),this)},addLayers:function(e,t){if(!L.Util.isArray(e))return this.addLayer(e);var i,n=this._featureGroup,r=this._nonPointGroup,s=this.options.chunkedLoading,o=this.options.chunkInterval,a=this.options.chunkProgress,h=e.length,l=0,u=!0;if(this._map){var _=(new Date).getTime(),d=L.bind(function(){for(var c=(new Date).getTime();h>l;l++){if(s&&0===l%200){var p=(new Date).getTime()-c;if(p>o)break}if(i=e[l],i instanceof L.LayerGroup)u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length;else if(i.getLatLng){if(!this.hasLayer(i)&&(this._addLayer(i,this._maxZoom),t||this.fire("layeradd",{layer:i}),i.__parent&&2===i.__parent.getChildCount())){var f=i.__parent.getAllChildMarkers(),m=f[0]===i?f[1]:f[0];n.removeLayer(m)}}else r.addLayer(i),t||this.fire("layeradd",{layer:i})}a&&a(l,h,(new Date).getTime()-_),l===h?(this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else for(var c=this._needsClustering;h>l;l++)i=e[l],i instanceof L.LayerGroup?(u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length):i.getLatLng?this.hasLayer(i)||c.push(i):r.addLayer(i);return this},removeLayers:function(e){var t,i,n=e.length,r=this._featureGroup,s=this._nonPointGroup,o=!0;if(!this._map){for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):(this._arraySplice(this._needsClustering,i),s.removeLayer(i),this.hasLayer(i)&&this._needsRemoving.push({layer:i,latlng:i._latlng}),this.fire("layerremove",{layer:i}));return this}if(this._unspiderfy){this._unspiderfy();var a=e.slice(),h=n;for(t=0;h>t;t++)i=a[t],i instanceof L.LayerGroup?(this._extractNonGroupLayers(i,a),h=a.length):this._unspiderfyLayer(i)}for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):i.__parent?(this._removeLayer(i,!0,!0),this.fire("layerremove",{layer:i}),r.hasLayer(i)&&(r.removeLayer(i),i.clusterShow&&i.clusterShow())):(s.removeLayer(i),this.fire("layerremove",{layer:i}));return this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),this},clearLayers:function(){return this._map||(this._needsClustering=[],this._needsRemoving=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(e){e.off(this._childMarkerEventHandlers,this),delete e.__parent},this),this._map&&this._generateInitialClusters(),this},getBounds:function(){var e=new L.LatLngBounds;this._topClusterLevel&&e.extend(this._topClusterLevel._bounds);for(var t=this._needsClustering.length-1;t>=0;t--)e.extend(this._needsClustering[t].getLatLng());return e.extend(this._nonPointGroup.getBounds()),e},eachLayer:function(e,t){var i,n,r,s=this._needsClustering.slice(),o=this._needsRemoving;for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(s),n=s.length-1;n>=0;n--){for(i=!0,r=o.length-1;r>=0;r--)if(o[r].layer===s[n]){i=!1;break}i&&e.call(t,s[n])}this._nonPointGroup.eachLayer(e,t)},getLayers:function(){var e=[];return this.eachLayer(function(t){e.push(t)}),e},getLayer:function(e){var t=null;return e=parseInt(e,10),this.eachLayer(function(i){L.stamp(i)===e&&(t=i)}),t},hasLayer:function(e){if(!e)return!1;var t,i=this._needsClustering;for(t=i.length-1;t>=0;t--)if(i[t]===e)return!0;for(i=this._needsRemoving,t=i.length-1;t>=0;t--)if(i[t].layer===e)return!1;return!(!e.__parent||e.__parent._group!==this)||this._nonPointGroup.hasLayer(e)},zoomToShowLayer:function(e,t){"function"!=typeof t&&(t=function(){});var i=function(){!e._icon&&!e.__parent._icon||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),e._icon?t():e.__parent._icon&&(this.once("spiderfied",t,this),e.__parent.spiderfy()))};e._icon&&this._map.getBounds().contains(e.getLatLng())?t():e.__parent._zoomt;t++)n=this._needsRemoving[t],n.newlatlng=n.layer._latlng,n.layer._latlng=n.latlng;for(t=0,i=this._needsRemoving.length;i>t;t++)n=this._needsRemoving[t],this._removeLayer(n.layer,!0),n.layer._latlng=n.newlatlng;this._needsRemoving=[],this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i,!0)},onRemove:function(e){e.off("zoomend",this._zoomEnd,this),e.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),delete this._maxLat,this._hideCoverage(),this._featureGroup.remove(),this._nonPointGroup.remove(),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(e){for(var t=e;t&&!t._icon;)t=t.__parent;return t||null},_arraySplice:function(e,t){for(var i=e.length-1;i>=0;i--)if(e[i]===t)return e.splice(i,1),!0},_removeFromGridUnclustered:function(e,t){for(var i=this._map,n=this._gridUnclustered,r=Math.floor(this._map.getMinZoom());t>=r&&n[t].removeObject(e,i.project(e.getLatLng(),t));t--);},_childMarkerDragStart:function(e){e.target.__dragStart=e.target._latlng},_childMarkerMoved:function(e){if(!this._ignoreMove&&!e.target.__dragStart){var t=e.target._popup&&e.target._popup.isOpen();this._moveChild(e.target,e.oldLatLng,e.latlng),t&&e.target.openPopup()}},_moveChild:function(e,t,i){e._latlng=t,this.removeLayer(e),e._latlng=i,this.addLayer(e)},_childMarkerDragEnd:function(e){var t=e.target.__dragStart;delete e.target.__dragStart,t&&this._moveChild(e.target,t,e.target._latlng)},_removeLayer:function(e,t,i){var n=this._gridClusters,r=this._gridUnclustered,s=this._featureGroup,o=this._map,a=Math.floor(this._map.getMinZoom());t&&this._removeFromGridUnclustered(e,this._maxZoom);var h,l=e.__parent,u=l._markers;for(this._arraySplice(u,e);l&&(l._childCount--,l._boundsNeedUpdate=!0,!(l._zoomt?"small":100>t?"medium":"large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(t||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){for(var t=e.layer,i=t;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),e.layer.getChildCount()>2&&e.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(e.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(e||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),t&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,Math.round(this._map._zoom),e),this._currentShownBounds=e}},_generateInitialClusters:function(){var e=Math.ceil(this._map.getMaxZoom()),t=Math.floor(this._map.getMinZoom()),i=this.options.maxClusterRadius,n=i;"function"!=typeof i&&(n=function(){return i}),null!==this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var r=e;r>=t;r--)this._gridClusters[r]=new L.DistanceGrid(n(r)),this._gridUnclustered[r]=new L.DistanceGrid(n(r));this._topClusterLevel=new this._markerCluster(this,t-1)},_addLayer:function(e,t){var i,n,r=this._gridClusters,s=this._gridUnclustered,o=Math.floor(this._map.getMinZoom());for(this.options.singleMarkerMode&&this._overrideMarkerIcon(e),e.on(this._childMarkerEventHandlers,this);t>=o;t--){i=this._map.project(e.getLatLng(),t);var a=r[t].getNearObject(i);if(a)return a._addChild(e),e.__parent=a,void 0;if(a=s[t].getNearObject(i)){var h=a.__parent;h&&this._removeLayer(a,!1);var l=new this._markerCluster(this,t,a,e);r[t].addObject(l,this._map.project(l._cLatLng,t)),a.__parent=l,e.__parent=l;var u=l;for(n=t-1;n>h._zoom;n--)u=new this._markerCluster(this,n,u),r[n].addObject(u,this._map.project(a.getLatLng(),n));return h._addChild(u),this._removeFromGridUnclustered(a,t),void 0}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,n=e.getLayers(),r=0;for(t=t||[];r=0;i--)o=h[i],n.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,t),r.eachLayer(function(e){e instanceof L.MarkerCluster||!e._icon||e.clusterShow()}),this._topClusterLevel._recursively(n,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),this._ignoreMove=!1,this._enqueue(function(){this._topClusterLevel._recursively(n,e,s,function(e){r.removeLayer(e),e.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),e,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){var i=this,n=this._featureGroup;n.addLayer(e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.clusterHide(),this._enqueue(function(){n.removeLayer(e),e.clusterShow(),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(t,this._map.getMaxZoom(),this._zoom)))}},_animationZoomOutSingle:function(e,t,i){var n=this._getExpandedVisibleBounds(),r=Math.floor(this._map.getMinZoom());e._recursivelyAnimateChildrenInAndAddSelfToMap(n,r,t+1,i);var s=this;this._forceLayout(),e._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===e._childCount){var o=e._markers[0];this._ignoreMove=!0,o.setLatLng(o.getLatLng()),this._ignoreMove=!1,o.clusterShow&&o.clusterShow()}else e._recursively(n,i,r,function(e){e._recursivelyRemoveChildrenFromMap(n,r,t+1)});s._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}),L.markerClusterGroup=function(e){return new L.MarkerClusterGroup(e)};var i=L.MarkerCluster=L.Marker.extend({options:L.Icon.prototype.options,initialize:function(e,t,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this,pane:e.options.clusterPane}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(e,t){e=e||[];for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i].getAllChildMarkers(e);for(var n=this._markers.length-1;n>=0;n--)t&&this._markers[n].__dragStart||e.push(this._markers[n]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(e){for(var t,i=this._childClusters.slice(),n=this._group._map,r=n.getBoundsZoom(this._bounds),s=this._zoom+1,o=n.getZoom();i.length>0&&r>s;){s++;var a=[];for(t=0;ts?this._group._map.setView(this._latlng,s):o>=r?this._group._map.setView(this._latlng,o+1):this._group._map.fitBounds(this._bounds,e)},getBounds:function(){var e=new L.LatLngBounds;return e.extend(this._bounds),e},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_setClusterCenter:function(e){this._cLatLng||(this._cLatLng=e._cLatLng||e._latlng)},_resetBounds:function(){var e=this._bounds;e._southWest&&(e._southWest.lat=1/0,e._southWest.lng=1/0),e._northEast&&(e._northEast.lat=-1/0,e._northEast.lng=-1/0)},_recalculateBounds:function(){var e,t,i,n,r=this._markers,s=this._childClusters,o=0,a=0,h=this._childCount;if(0!==h){for(this._resetBounds(),e=0;e=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())},function(e){var i,n,r=e._childClusters;for(i=r.length-1;i>=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,i,n){this._recursively(e,n,t,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),i),r._isSingleParent()&&i-1===n?(r.clusterShow(),r._recursivelyRemoveChildrenFromMap(e,t,i)):r.clusterHide(),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,this._group._map.getMinZoom(),t,null,function(e){e.clusterShow()})},_recursivelyAddChildrenToMap:function(e,t,i){this._recursively(i,this._group._map.getMinZoom()-1,t,function(n){if(t!==n._zoom)for(var r=n._markers.length-1;r>=0;r--){var s=n._markers[r];i.contains(s._latlng)&&(e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.clusterHide&&s.clusterHide()),n._group._featureGroup.addLayer(s))}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var i=this._markers[t];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(e-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,i,n){var r,s;this._recursively(e,t-1,i-1,function(e){for(s=e._markers.length-1;s>=0;s--)r=e._markers[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())},function(e){for(s=e._childClusters.length-1;s>=0;s--)r=e._childClusters[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())})},_recursively:function(e,t,i,n,r){var s,o,a=this._childClusters,h=this._zoom;if(h>=t&&(n&&n(this),r&&h===i&&r(this)),t>h||i>h)for(s=a.length-1;s>=0;s--)o=a[s],o._boundsNeedUpdate&&o._recalculateBounds(),e.intersects(o._bounds)&&o._recursively(e,t,i,n,r)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}});L.Marker.include({clusterHide:function(){var e=this.options.opacity;return this.setOpacity(0),this.options.opacity=e,this},clusterShow:function(){return this.setOpacity(this.options.opacity)}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var i=this._getCoord(t.x),n=this._getCoord(t.y),r=this._grid,s=r[n]=r[n]||{},o=s[i]=s[i]||[],a=L.Util.stamp(e);this._objectPoint[a]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var i,n,r=this._getCoord(t.x),s=this._getCoord(t.y),o=this._grid,a=o[s]=o[s]||{},h=a[r]=a[r]||[];for(delete this._objectPoint[L.Util.stamp(e)],i=0,n=h.length;n>i;i++)if(h[i]===e)return h.splice(i,1),1===n&&delete a[r],!0},eachObject:function(e,t){var i,n,r,s,o,a,h,l=this._grid;for(i in l){o=l[i];for(n in o)for(a=o[n],r=0,s=a.length;s>r;r++)h=e.call(t,a[r]),h&&(r--,s--)}},getNearObject:function(e){var t,i,n,r,s,o,a,h,l=this._getCoord(e.x),u=this._getCoord(e.y),_=this._objectPoint,d=this._sqCellSize,c=null;for(t=u-1;u+1>=t;t++)if(r=this._grid[t])for(i=l-1;l+1>=i;i++)if(s=r[i])for(n=0,o=s.length;o>n;n++)a=s[n],h=this._sqDist(_[L.Util.stamp(a)],e),(d>h||d>=h&&null===c)&&(d=h,c=a);return c},_getCoord:function(e){var t=Math.floor(e/this._cellSize);return isFinite(t)?t:e},_sqDist:function(e,t){var i=t.x-e.x,n=t.y-e.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(e,t){var i=t[1].lat-t[0].lat,n=t[0].lng-t[1].lng;return n*(e.lat-t[0].lat)+i*(e.lng-t[0].lng)},findMostDistantPointFromBaseLine:function(e,t){var i,n,r,s=0,o=null,a=[];for(i=t.length-1;i>=0;i--)n=t[i],r=this.getDistant(n,e),r>0&&(a.push(n),r>s&&(s=r,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(e,t){var i=[],n=this.findMostDistantPointFromBaseLine(e,t);return n.maxPoint?(i=i.concat(this.buildConvexHull([e[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,e[1]],n.newPoints))):[e[0]]},getConvexHull:function(e){var t,i=!1,n=!1,r=!1,s=!1,o=null,a=null,h=null,l=null,u=null,_=null;for(t=e.length-1;t>=0;t--){var d=e[t];(i===!1||d.lat>i)&&(o=d,i=d.lat),(n===!1||d.latr)&&(h=d,r=d.lng),(s===!1||d.lng=0;t--)e=i[t].getLatLng(),n.push(e);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:0,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var e,t=this.getAllChildMarkers(null,!0),i=this._group,n=i._map,r=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,t.length>=this._circleSpiralSwitchover?e=this._generatePointsSpiral(t.length,r):(r.y+=10,e=this._generatePointsCircle(t.length,r)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,n,r=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),s=r/this._2PI,o=this._2PI/e,a=[];for(s=Math.max(s,35),a.length=e,i=0;e>i;i++)n=this._circleStartAngle+i*o,a[i]=new L.Point(t.x+s*Math.cos(n),t.y+s*Math.sin(n))._round();return a},_generatePointsSpiral:function(e,t){var i,n=this._group.options.spiderfyDistanceMultiplier,r=n*this._spiralLengthStart,s=n*this._spiralFootSeparation,o=n*this._spiralLengthFactor*this._2PI,a=0,h=[];for(h.length=e,i=e;i>=0;i--)e>i&&(h[i]=new L.Point(t.x+r*Math.cos(a),t.y+r*Math.sin(a))._round()),a+=s/r+5e-4*i,r+=o/a;return h},_noanimationUnspiderfy:function(){var e,t,i=this._group,n=i._map,r=i._featureGroup,s=this.getAllChildMarkers(null,!0);for(i._ignoreMove=!0,this.setOpacity(1),t=s.length-1;t>=0;t--)e=s[t],r.removeLayer(e),e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng),e.setZIndexOffset&&e.setZIndexOffset(0),e._spiderLeg&&(n.removeLayer(e._spiderLeg),delete e._spiderLeg);i.fire("unspiderfied",{cluster:this,markers:s}),i._ignoreMove=!1,i._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(e,t){var i,n,r,s,o=this._group,a=o._map,h=o._featureGroup,l=this._group.options.spiderLegPolylineOptions;for(o._ignoreMove=!0,i=0;i=0;i--)a=u.layerPointToLatLng(t[i]),n=e[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(a),n.clusterShow&&n.clusterShow(),p&&(r=n._spiderLeg,s=r._path,s.style.strokeDashoffset=0,r.setStyle({opacity:m}));this.setOpacity(.3),l._ignoreMove=!1,setTimeout(function(){l._animationEnd(),l.fire("spiderfied",{cluster:h,markers:e})},200)},_animationUnspiderfy:function(e){var t,i,n,r,s,o,a=this,h=this._group,l=h._map,u=h._featureGroup,_=e?l._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):l.latLngToLayerPoint(this._latlng),d=this.getAllChildMarkers(null,!0),c=L.Path.SVG;for(h._ignoreMove=!0,h._animationStart(),this.setOpacity(1),i=d.length-1;i>=0;i--)t=d[i],t._preSpiderfyLatlng&&(t.closePopup(),t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng,o=!0,t._setPos&&(t._setPos(_),o=!1),t.clusterHide&&(t.clusterHide(),o=!1),o&&u.removeLayer(t),c&&(n=t._spiderLeg,r=n._path,s=r.getTotalLength()+.1,r.style.strokeDashoffset=s,n.setStyle({opacity:0})));h._ignoreMove=!1,setTimeout(function(){var e=0;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&e++;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&(t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),e>1&&u.removeLayer(t),l.removeLayer(t._spiderLeg),delete t._spiderLeg);h._animationEnd(),h.fire("unspiderfied",{cluster:a,markers:d})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,unspiderfy:function(){this._unspiderfy.apply(this,arguments)},_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Browser.touch||this._map.getRenderer(this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy() -},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(e){e._spiderLeg&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}}),L.MarkerClusterGroup.include({refreshClusters:function(e){return e?e instanceof L.MarkerClusterGroup?e=e._topClusterLevel.getAllChildMarkers():e instanceof L.LayerGroup?e=e._layers:e instanceof L.MarkerCluster?e=e.getAllChildMarkers():e instanceof L.Marker&&(e=[e]):e=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(e),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(e),this},_flagParentsIconsNeedUpdate:function(e){var t,i;for(t in e)for(i=e[t].__parent;i;)i._iconNeedsUpdate=!0,i=i.__parent},_refreshSingleMarkerModeMarkers:function(e){var t,i;for(t in e)i=e[t],this.hasLayer(i)&&i.setIcon(this._overrideMarkerIcon(i))}}),L.Marker.include({refreshIconOptions:function(e,t){var i=this.options.icon;return L.setOptions(i,e),this.setIcon(i),t&&this.__parent&&this.__parent._group.refreshClusters(this),this}}),e.MarkerClusterGroup=t,e.MarkerCluster=i}); -//# sourceMappingURL=leaflet.markercluster.js.map diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index c8d185da..37deb7e0 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -90,7 +90,6 @@ Adds cluster charts (either pie or bar charts) to a Leaflet map. \examples{ # Example usage: library(sf) -library(geojsonsf) library(leaflet) library(leaflet.extras2) diff --git a/man/addTimeslider.Rd b/man/addTimeslider.Rd index 6338b01a..64a99d36 100644 --- a/man/addTimeslider.Rd +++ b/man/addTimeslider.Rd @@ -84,7 +84,6 @@ JQuery UI slider. library(leaflet) library(leaflet.extras2) library(sf) -library(geojsonsf) data <- sf::st_as_sf(leaflet::atlStorms2005[1,]) data <- st_cast(data, "POINT") diff --git a/tests/testthat/test-timeslider.R b/tests/testthat/test-timeslider.R index acd705e8..85c70f1f 100644 --- a/tests/testthat/test-timeslider.R +++ b/tests/testthat/test-timeslider.R @@ -15,7 +15,6 @@ test_that("timeslider", { expect_is(m, "leaflet") expect_identical(m$x$calls[[1]]$method, "addTimeslider") expect_is(m$x$calls[[1]]$args[[1]], "geojson") - # expect_identical(m$x$calls[[1]]$args[[1]], sf_geojson(data)) expect_true(inherits(m$x$calls[[1]]$args[[1]], "geojson")) m <- leaflet() %>% @@ -29,7 +28,6 @@ test_that("timeslider", { expect_is(m, "leaflet") expect_identical(m$x$calls[[1]]$method, "addTimeslider") expect_is(m$x$calls[[1]]$args[[1]], "geojson") - # expect_identical(m$x$calls[[1]]$args[[1]], sf_geojson(data)) expect_true(inherits(m$x$calls[[1]]$args[[1]], "geojson")) @@ -45,7 +43,6 @@ test_that("timeslider", { expect_is(m, "leaflet") expect_identical(m$x$calls[[1]]$method, "addTimeslider") expect_is(m$x$calls[[1]]$args[[1]], "geojson") - # expect_identical(m$x$calls[[1]]$args[[1]], sf_geojson(data)) expect_true(inherits(m$x$calls[[1]]$args[[1]], "geojson")) m <- m %>% From 06b746f81509ae50bbbb196aae7fb30eb92c9f4c Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Fri, 9 Aug 2024 20:04:37 +0200 Subject: [PATCH 10/26] feat: add icon, html, sortTitlebyCount --- R/clusterCharts.R | 96 ++++++++++++++++--- inst/examples/clusterCharts_app.R | 60 +++++++++--- .../lfx-clustercharts-bindings.js | 86 +++++++++++++++-- man/addClusterCharts.Rd | 12 +++ man/clusterchartOptions.Rd | 3 +- 5 files changed, 222 insertions(+), 35 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 12d1d790..77acdea2 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -72,6 +72,7 @@ clusterchartsDependencies <- function() { addClusterCharts <- function( map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal"), options = clusterchartOptions(), + icon = NULL, html = NULL, popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, clusterOptions = NULL, clusterId = NULL, categoryField, categoryMap, popupFields = NULL, popupLabels = NULL, @@ -107,13 +108,14 @@ addClusterCharts <- function( } ## CSS string ############# - css <- paste(apply(categoryMap, 1, generate_css), collapse = "\n") + css <- paste(apply(categoryMap, 1, generate_css, icon), collapse = "\n") size <- options$size if (length(size) == 1) size <- rep(size, 2) css <- paste0(css, "\n.clustermarker {", "width: ",size[1],"px; height: ",size[2],"px;", - "margin-top: -",size[1]/2,"px; margin-left: -",size[2]/2,"px;", + "margin-top: -",size[2]/2,"px; margin-left: -",size[1]/2,"px;", "}") + csssrc <- list( htmltools::htmlDependency( "lfx-clustercharts-css", version = "1.0.0", @@ -142,7 +144,7 @@ addClusterCharts <- function( "addClusterCharts") leaflet::invokeMethod( map, NULL, "addClusterCharts", geojson, layerId, group, type, - options, + options, icon, html, popup, popupOptions, safeLabel(label, data), labelOptions, clusterOptions, clusterId, categoryField, categoryMapList, popupFields, popupLabels, @@ -168,7 +170,8 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), labelFill = "white", labelStroke = "black", labelColor = "black", - labelOpacity = 0.9) { + labelOpacity = 0.9, + sortTitlebyCount = TRUE) { filterNULL(list( rmax = rmax , size = size @@ -181,20 +184,23 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), , labelStroke = labelStroke , labelColor = labelColor , labelOpacity = labelOpacity + , sortTitlebyCount = sortTitlebyCount )) } -generate_css <- function(row) { +generate_css <- function(row, icon) { + ## Get/Check Inputs ############ label <- row["labels"] color <- row["colors"] stroke <- row["strokes"] - icon <- row['icons'] if (is.null(color)) color <- stroke if (is.null(stroke)) stroke <- color - # Replace spaces with dots in the class name - class_name <- paste0("category-",gsub(" ", ".", label)) + ## Replace spaces with dots in the class name ####### + label_nospaces <- gsub(" ", ".", label, fixed = TRUE) + ## Make Custom CSS-class with fill/stroke/background ################ + class_name <- paste0("category-", label_nospaces) css <- paste0( ".", class_name, " {\n", " fill: ", color, ";\n", @@ -203,15 +209,77 @@ generate_css <- function(row) { " border-color: ", stroke, ";\n", "}\n" ) - if (!is.null(icon)) { + + ## Make Icon ################ + if (is.null(icon)) { + icon <- row['icons'] + if (!is.null(icon)) { + css <- paste0(css, + ".icon-", label_nospaces, " {\n", + " background-image: url('", icon, "');\n", + " background-repeat: no-repeat;\n", + " background-position: 0px 1px;\n", + "}" + ) + # css <- backgroundCSS(label_nospaces, icon, + # additional_css = list( + # c("background-blend-mode", "color-burn"), + # c("opacity", "0.8"), + # c("border-radius", "5px") + # )) + } + } else { + if (inherits(icon, "leaflet_icon_set")) { + icon <- icon[[label]] + } + iconuse <- b64EncodePackedIcons(packStrings(icon$iconUrl)) + size = "" + names_icon <- names(icon) + if ("iconWidth" %in% names_icon) { + if ("iconHeight" %in% names_icon) { + size <- paste0("background-size: ", icon$iconWidth, "px ", icon$iconHeight, "px;\n") + } else { + size <- paste0("background-size: ", icon$iconWidth, "px ", icon$iconWidth, "px;\n") + } + } css <- paste0(css, - ".icon-", gsub(" ", ".", label), " {\n", - " background-image: url('", icon, "') !important;\n", - " background-repeat: no-repeat !important;\n", - " background-position: 0px 1px !important;\n", + ".icon-", label_nospaces, " {\n", + " background-image: url('", iconuse$data, "');\n", + " background-repeat: no-repeat;\n", + " background-position: 0px 1px;\n", + size, "}" - ) + ) } + # cat(css) css } +iconSetToIcons <- utils::getFromNamespace("iconSetToIcons", "leaflet") +b64EncodePackedIcons <- utils::getFromNamespace("b64EncodePackedIcons", "leaflet") +packStrings <- utils::getFromNamespace("packStrings", "leaflet") + + +backgroundCSS <- function(label, icon, + background_repeat = "no-repeat", + background_position = "0px 1px", + additional_css = list()) { + # Start the CSS string + css <- paste0(".icon-", label, " {\n", + " background-image: url('", icon, "');\n", + " background-repeat: ", background_repeat, ";\n", + " background-position: ", background_position, ";\n") + + # Add each additional CSS property + for (css_property in additional_css) { + css <- paste0(css, " ", css_property[1], ": ", css_property[2], ";\n") + } + + # Close the CSS block + css <- paste0(css, "}") + + return(css) +} + + + diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index f730bd4c..00166349 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -1,17 +1,48 @@ library(shiny) library(sf) library(leaflet) +library(leaflet.extras) library(leaflet.extras2) options("shiny.autoreload" = TRUE) + +# shipIcon <- leaflet::makeIcon( +# iconUrl = "./icons/Icon5.svg" +# ,className = "lsaicons" +# ,iconWidth = 24, iconHeight = 24, iconAnchorX = 0, iconAnchorY = 0 +# ) +shipIcon <- iconList( + "Schwer" = makeIcon("./icons/Icon5.svg", iconWidth = 32, iconHeight = 32), + "Mäßig" = makeIcon("./icons/Icon8.svg", iconWidth = 32, iconHeight = 32), + "Leicht" = makeIcon("./icons/Icon25.svg", iconWidth = 32, iconHeight = 32), + "kein Schaden" = makeIcon("./icons/Icon29.svg", iconWidth = 32, iconHeight = 32) +) +# shipIcon <- makeIcon( +# iconUrl = "https://cdn-icons-png.flaticon.com/512/1355/1355883.png", +# iconWidth = 40, iconHeight = 50, +# iconAnchorX = 0, iconAnchorY = 0 +# ) + data <- sf::st_as_sf(breweries91) data$category <- sample(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), size = nrow(data), replace = TRUE) data$label <- paste0(data$brewery, "
", data$address) data$id <- paste0("ID", seq.int(nrow(data))) data$popup <- paste0("
", data$brewery, "
", data$address, "
") +data$web <- gsub(">(.*?)<", ">LINK<", data$web) +data$web <- ifelse(is.na(data$web), "", paste0("
", data$web, "
")) ui <- fluidPage( - leafletOutput("map", height = 500), + tags$head(tags$style(" + .clusterchartsicon { + background-position: left !important; + } + .markerhtml { + height: 100%; + margin-top: 8px; + left: 41px; + position: absolute; + }")), + leafletOutput("map", height = 650), splitLayout(cellWidths = paste0(rep(20,4), "%"), div(verbatimTextOutput("click")), div(verbatimTextOutput("mouseover")), @@ -24,27 +55,32 @@ server <- function(input, output, session) { output$map <- renderLeaflet({ leaflet() %>% addMapPane("clusterpane", 420) %>% addProviderTiles("CartoDB") %>% - leaflet::addLayersControl(overlayGroups = "clustermarkers") %>% - # addCircleMarkers(data = data, clusterOptions = markerClusterOptions()) %>% + leaflet::addLayersControl(overlayGroups = c("clustermarkers","normalcircles")) %>% + addCircleMarkers(data = data, group = "normalcircles", clusterOptions = markerClusterOptions()) %>% addClusterCharts(data = data - , options = clusterchartOptions(rmax = 40, size = 30, - width = 30, height = 30, - strokeWidth = 2, + , options = clusterchartOptions(rmax = 40, + size = c(100,40), + # size=40, + # width = 70, height = 30, + strokeWidth = 1, labelBackground = T, - labelFill = "orange", - labelStroke = "gray10", + # labelFill = "white", + # labelStroke = "green", labelColor = "blue", labelOpacity = 0.5, - innerRadius = 10) + innerRadius = 6, + sortTitlebyCount = T) # , type = "bar" - , type = "horizontal" + # , type = "horizontal" , categoryField = "category" + , html = "web" + , icon = shipIcon , categoryMap = data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), colors = c("#F88", "#FA0", "#FF3", "#BFB"), # colors = c("lightblue", "orange", "lightyellow", "lightgreen"), # colors = c("cyan", "darkorange", "yellow", "#9fca8b"), - icons = c("icons/Icon29.svg", "icons/Icon8.svg", "icons/Icon5.svg", "icons/Icon25.svg"), + # icons = c("icons/Icon29.svg", "icons/Icon8.svg", "icons/Icon5.svg", "icons/Icon25.svg"), # strokes = c("#800", "#B60", "#D80", "#070") strokes = "gray" ) @@ -72,7 +108,7 @@ server <- function(input, output, session) { removeOutsideVisibleBounds = TRUE, spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5), freezeAtZoom = TRUE, - # clusterPane = "clusterpane", + clusterPane = "clusterpane", spiderfyDistanceMultiplier = 2 ) , labelOptions = labelOptions(opacity = 0.8, textsize = "14px") diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 50d3e412..cad1b4c0 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -1,6 +1,6 @@ /* global LeafletWidget, $, L */ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, - options, + options, icon, html, popup, popupOptions, label, labelOptions, clusterOptions, clusterId, categoryField, categoryMap, popupFields, popupLabels, @@ -16,6 +16,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var labelStroke = options.labelStroke ? options.labelStroke : "black"; var labelColor = options.labelColor ? options.labelColor : "black"; var labelOpacity = options.labelOpacity ? options.labelOpacity : 0.9; + var sortTitlebyCount = options.sortTitlebyCount ? options.sortTitlebyCount : false; // Make L.markerClusterGroup, markers, fitBounds and renderLegend console.log("geojson"); console.log(geojson) @@ -49,13 +50,14 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Functions function defineFeature(feature, latlng) { var categoryVal = feature.properties[categoryField] - var myClass = 'clustermarker category-'+categoryVal+' icon-'+categoryVal; - //console.log("myClass"); console.log(myClass) + var myClass = 'clustermarker category-'+categoryVal+' clusterchartsicon icon-'+categoryVal; let extraInfo = { clusterId: clusterId }; + console.log("feature"); console.log(feature) // Make DIV-Icon marker var myIcon = L.divIcon({ className: myClass, + html: feature.properties[html] ? feature.properties[html] : "", iconSize: null }); var marker = L.marker(latlng, @@ -231,12 +233,34 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .append('svg:title') .text(pathTitleFunc); - // Bar Label Background + // Create Title for Individual Elements and All in Cluster console.log("d3pie data") pathTitleFunc = function(d){ return d.key + ' (' + d.values.length + ')'; - } - var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); + } + let allTitles = "" + if (sortTitlebyCount) { + allTitles = data + .sort(function(a, b) { return b.values.length - a.values.length; }) // Sort by length in descending order + .map(function(d) { return pathTitleFunc(d); }) + .join('\n'); + } else { + let categoryOrder = {}; + for (var key in categoryMap) { + categoryOrder[categoryMap[key]] = parseInt(key); + } + allTitles = data + .sort(function(a, b) { + // Get the order values from categoryOrder + var orderA = categoryOrder[a.key] || Infinity; + var orderB = categoryOrder[b.key] || Infinity; + return orderA - orderB; // Sort in ascending order + }) + .map(function(d) { return pathTitleFunc(d); }) + .join('\n'); + } + + // Show Label Background if (labelBackground && labelBackground == true) { vis.append('circle') .attr('r', rInner-5) @@ -301,8 +325,31 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .append('svg:title') .text(pathTitleFunc); + // Create Title for Individual Elements and All in Cluster + let allTitles = "" + //var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); + if (sortTitlebyCount) { + allTitles = data + .sort(function(a, b) { return b.values.length - a.values.length; }) // Sort by length in descending order + .map(function(d) { return pathTitleFunc(d); }) + .join('\n'); + } else { + let categoryOrder = {}; + for (var key in categoryMap) { + categoryOrder[categoryMap[key]] = parseInt(key); + } + allTitles = data + .sort(function(a, b) { + // Get the order values from categoryOrder + var orderA = categoryOrder[a.key] || Infinity; + var orderB = categoryOrder[b.key] || Infinity; + return orderA - orderB; // Sort in ascending order + }) + .map(function(d) { return pathTitleFunc(d); }) + .join('\n'); + } + // Bar Label Background - var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); if (labelBackground && labelBackground == true) { vis.append('rect') .attr('x', 0) // Adjust the width of the background @@ -369,8 +416,31 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .append('svg:title') .text(pathTitleFunc); + // Create Title for Individual Elements and All in Cluster + let allTitles = "" + //var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); + if (sortTitlebyCount) { + allTitles = data + .sort(function(a, b) { return b.values.length - a.values.length; }) // Sort by length in descending order + .map(function(d) { return pathTitleFunc(d); }) + .join('\n'); + } else { + let categoryOrder = {}; + for (var key in categoryMap) { + categoryOrder[categoryMap[key]] = parseInt(key); + } + allTitles = data + .sort(function(a, b) { + // Get the order values from categoryOrder + var orderA = categoryOrder[a.key] || Infinity; + var orderB = categoryOrder[b.key] || Infinity; + return orderA - orderB; // Sort in ascending order + }) + .map(function(d) { return pathTitleFunc(d); }) + .join('\n'); + } + // Bar Label Background - var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); if (labelBackground && labelBackground == true) { vis.append('rect') .attr('x', 0) // Adjust the width of the background diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 37deb7e0..21dde5e4 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -12,6 +12,8 @@ addClusterCharts( group = NULL, type = c("pie", "bar", "horizontal"), options = clusterchartOptions(), + icon = NULL, + html = NULL, popup = NULL, popupOptions = NULL, label = NULL, @@ -52,6 +54,16 @@ layers (e.g. markers and polygons) can share the same group name.} \item{options}{Additional options for cluster charts (see \code{\link{clusterchartOptions}}).} +\item{icon}{the icon(s) for markers; an icon is represented by an R list of +the form \code{list(iconUrl = "?", iconSize = c(x, y))}, and you can use +\code{\link[leaflet]{icons}()} to create multiple icons; note when you use an R list +that contains images as local files, these local image files will be base64 +encoded into the HTML page so the icon images will still be available even +when you publish the map elsewhere} + +\item{html}{the content of the control. May be provided as string or as HTML +generated with Shiny/htmltools tags} + \item{popup}{Use the column name given in popup to collect the feature property with this name.} \item{popupOptions}{A Vector of \code{\link[leaflet]{popupOptions}} to provide popups} diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index 676f86f9..0b7550a8 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -15,7 +15,8 @@ clusterchartOptions( labelFill = "white", labelStroke = "black", labelColor = "black", - labelOpacity = 0.9 + labelOpacity = 0.9, + sortTitlebyCount = TRUE ) } \arguments{ From c0b019b1ddf274db79cda737fbcb1ffe5abc94e4 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Fri, 9 Aug 2024 20:32:26 +0200 Subject: [PATCH 11/26] fix widh/height with biger strokewidth, adjust rect-labels (centered) --- .../lfx-clustercharts-bindings.js | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index cad1b4c0..c0eb2451 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -310,8 +310,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var svg = document.createElementNS(d3.ns.prefix.svg, "svg"); var vis = d3.select(svg) .attr('class', barClass) - .attr('width', width) - .attr('height', height + 20); + .attr('width', width + strokeWidth) + .attr('height', height + 20 + strokeWidth); // Bars vis.selectAll('.bar') @@ -327,7 +327,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Create Title for Individual Elements and All in Cluster let allTitles = "" - //var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); if (sortTitlebyCount) { allTitles = data .sort(function(a, b) { return b.values.length - a.values.length; }) // Sort by length in descending order @@ -352,9 +351,9 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Bar Label Background if (labelBackground && labelBackground == true) { vis.append('rect') - .attr('x', 0) // Adjust the width of the background - .attr('y', 35) // Adjust the y position for the background - .attr('width', 30) // Width of the background + .attr('x', (width - (width - 10)) / 2) // Adjust the width of the background + .attr('y', height + 5) // Adjust the y position for the background + .attr('width', width - 10) // Width of the background .attr('height', 15) // Height of the background .attr('fill', labelFill) .attr('stroke', labelStroke) @@ -367,7 +366,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Bar Label vis.append('text') .attr('x', width / 2) - .attr('y', 43) // Adjust the y position for the text + .attr('y', height + 13) // Adjust the y position for the text .attr('class', barLabelClass) .attr('text-anchor', 'middle') .attr('dy', '.3em') @@ -401,8 +400,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var svg = document.createElementNS(d3.ns.prefix.svg, "svg"); var vis = d3.select(svg) .attr('class', barClass) - .attr('width', width) - .attr('height', height + 20); + .attr('width', width + strokeWidth) + .attr('height', height + 20 + strokeWidth); // Bars vis.selectAll('.bar') @@ -444,7 +443,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (labelBackground && labelBackground == true) { vis.append('rect') .attr('x', 0) // Adjust the width of the background - .attr('y', height) // Adjust the y position for the background + .attr('y', height + 3) // Adjust the y position for the background .attr('width', width) // Width of the background .attr('height', 15) // Height of the background .attr('fill', labelFill) @@ -458,7 +457,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Bar Label vis.append('text') .attr('x', width / 2) - .attr('y', (height + 5)) // Adjust the y position for the text + .attr('y', (height + 8)) // Adjust the y position for the text .attr('class', barLabelClass) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') From e85ba8b7dafc871c436bd614ecdccae71ccc5d6e Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Fri, 9 Aug 2024 21:10:41 +0200 Subject: [PATCH 12/26] adapt app --- inst/examples/clusterCharts_app.R | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index 00166349..3bc442f3 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -30,6 +30,7 @@ data$id <- paste0("ID", seq.int(nrow(data))) data$popup <- paste0("
", data$brewery, "
", data$address, "
") data$web <- gsub(">(.*?)<", ">LINK<", data$web) data$web <- ifelse(is.na(data$web), "", paste0("
", data$web, "
")) +data$tosum <- sample(1:100, nrow(data), replace = TRUE) ui <- fluidPage( tags$head(tags$style(" @@ -56,19 +57,19 @@ server <- function(input, output, session) { leaflet() %>% addMapPane("clusterpane", 420) %>% addProviderTiles("CartoDB") %>% leaflet::addLayersControl(overlayGroups = c("clustermarkers","normalcircles")) %>% - addCircleMarkers(data = data, group = "normalcircles", clusterOptions = markerClusterOptions()) %>% + # addCircleMarkers(data = data, group = "normalcircles", clusterOptions = markerClusterOptions()) %>% addClusterCharts(data = data - , options = clusterchartOptions(rmax = 40, + , options = clusterchartOptions(rmax = 50, size = c(100,40), # size=40, - # width = 70, height = 30, + width = 100, height = 30, strokeWidth = 1, labelBackground = T, - # labelFill = "white", + # labelFill = "red", # labelStroke = "green", labelColor = "blue", labelOpacity = 0.5, - innerRadius = 6, + innerRadius = 10, sortTitlebyCount = T) # , type = "bar" # , type = "horizontal" @@ -96,20 +97,21 @@ server <- function(input, output, session) { , markerOptions = markerOptions(interactive = TRUE, draggable = TRUE, keyboard = TRUE, + # stroke = 0.1, title = "Some Marker Title", zIndexOffset = 100, opacity = 1, riseOnHover = TRUE, riseOffset = 400) , legendOptions = list(position = "bottomright", title = "Unfälle im Jahr 2003") - , clusterOptions = markerClusterOptions(showCoverageOnHover = TRUE, - zoomToBoundsOnClick = TRUE, - spiderfyOnMaxZoom = TRUE, - removeOutsideVisibleBounds = TRUE, - spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5), - freezeAtZoom = TRUE, - clusterPane = "clusterpane", - spiderfyDistanceMultiplier = 2 + , clusterOptions = markerClusterOptions(showCoverageOnHover = TRUE + , zoomToBoundsOnClick = TRUE + , spiderfyOnMaxZoom = TRUE + , removeOutsideVisibleBounds = TRUE + , spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5) + , freezeAtZoom = TRUE + , clusterPane = "clusterpane" + , spiderfyDistanceMultiplier = 1 ) , labelOptions = labelOptions(opacity = 0.8, textsize = "14px") , popupOptions = popupOptions(maxWidth = 900, minWidth = 200, keepInView = TRUE) From 1e7309246a3699fb7b5ec8dd393907e082ae4f83 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sat, 10 Aug 2024 09:36:38 +0200 Subject: [PATCH 13/26] feat: custom func/aggregation --- R/clusterCharts.R | 9 +- inst/examples/clusterCharts_app.R | 6 +- .../lfx-clustercharts-bindings.js | 283 ++++++++++++++---- man/addClusterCharts.Rd | 3 +- man/clusterchartOptions.Rd | 1 + 5 files changed, 236 insertions(+), 66 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 77acdea2..3a25a228 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -70,9 +70,10 @@ clusterchartsDependencies <- function() { #' , label = "brewery" #' ) addClusterCharts <- function( - map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal"), + map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal","custom"), options = clusterchartOptions(), icon = NULL, html = NULL, + customFunc = NULL, popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, clusterOptions = NULL, clusterId = NULL, categoryField, categoryMap, popupFields = NULL, popupLabels = NULL, @@ -146,7 +147,7 @@ addClusterCharts <- function( map, NULL, "addClusterCharts", geojson, layerId, group, type, options, icon, html, popup, popupOptions, safeLabel(label, data), labelOptions, - clusterOptions, clusterId, + clusterOptions, clusterId, customFunc, categoryField, categoryMapList, popupFields, popupLabels, markerOptions, legendOptions ) %>% @@ -171,6 +172,7 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), labelStroke = "black", labelColor = "black", labelOpacity = 0.9, + aggregation = "sum", sortTitlebyCount = TRUE) { filterNULL(list( rmax = rmax @@ -184,6 +186,7 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), , labelStroke = labelStroke , labelColor = labelColor , labelOpacity = labelOpacity + , aggregation = aggregation , sortTitlebyCount = sortTitlebyCount )) } @@ -251,7 +254,7 @@ generate_css <- function(row, icon) { "}" ) } - # cat(css) + cat(css) css } diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index 3bc442f3..8a6acaf6 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -24,7 +24,7 @@ shipIcon <- iconList( # ) data <- sf::st_as_sf(breweries91) -data$category <- sample(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), size = nrow(data), replace = TRUE) +data$categoryfields <- sample(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), size = nrow(data), replace = TRUE) data$label <- paste0(data$brewery, "
", data$address) data$id <- paste0("ID", seq.int(nrow(data))) data$popup <- paste0("
", data$brewery, "
", data$address, "
") @@ -73,7 +73,7 @@ server <- function(input, output, session) { sortTitlebyCount = T) # , type = "bar" # , type = "horizontal" - , categoryField = "category" + , categoryField = "categoryfields" , html = "web" , icon = shipIcon , categoryMap = @@ -91,7 +91,7 @@ server <- function(input, output, session) { , clusterId = "id" , popupFields = c("brewery","address","zipcode", "category") , popupLabels = c("Brauerei","Addresse","PLZ", "Art") - # , popup = "popup" + , popup = "popup" , label = "label" ## Options ############# , markerOptions = markerOptions(interactive = TRUE, diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index c0eb2451..fdbe28af 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -2,7 +2,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, options, icon, html, popup, popupOptions, label, labelOptions, - clusterOptions, clusterId, + clusterOptions, clusterId, customFunc, categoryField, categoryMap, popupFields, popupLabels, markerOptions, legendOptions) { @@ -17,9 +17,11 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var labelColor = options.labelColor ? options.labelColor : "black"; var labelOpacity = options.labelOpacity ? options.labelOpacity : 0.9; var sortTitlebyCount = options.sortTitlebyCount ? options.sortTitlebyCount : false; + var aggregation = options.aggregation ? options.aggregation : "sum"; // Make L.markerClusterGroup, markers, fitBounds and renderLegend console.log("geojson"); console.log(geojson) + console.log("clusterOptions"); console.log(clusterOptions) var markerclusters = L.markerClusterGroup( Object.assign({ maxClusterRadius: 2 * rmax, @@ -53,7 +55,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var myClass = 'clustermarker category-'+categoryVal+' clusterchartsicon icon-'+categoryVal; let extraInfo = { clusterId: clusterId }; - console.log("feature"); console.log(feature) + //console.log("feature"); console.log(feature) // Make DIV-Icon marker var myIcon = L.divIcon({ className: myClass, @@ -95,6 +97,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (popup && props[popup]) { popupContent = props[popup]; } else if (popupFields !== null ) { + console.log("popupFields"); console.log(popupFields) popupContent += ''; popupFields.map( function(key, idx) { if (props[key]) { @@ -120,62 +123,116 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var r = rmax-2*strokeWidth-(n<10?12:n<100?8:n<1000?4:0), //Calculate clusterpie radius... iconDim = (r+strokeWidth)*2, //...and divIcon dimensions (leaflet really want to know the size) html; + var innerRadius = options.innerRadius ? options.innerRadius : -10; //bake some svg markup - var data = d3.nest() //Build a dataset for the pie chart - .key(function(d) { return d.feature.properties[categoryField]; }) - .entries(children, d3.map) - - if (type == "pie") { - console.log("Piechart") - var innerRadius = options.innerRadius ? options.innerRadius : -10; - html = bakeThePie({ - data: data, - valueFunc: function(d){return d.values.length;}, - outerRadius: r, - innerRadius: r-innerRadius, - pieClass: 'cluster-pie', - pieLabel: n, - pieLabelClass: 'clustermarker-cluster-pie-label', - pathClassFunc: function(d){ - return "category-" + d.data.key; - }, - pathTitleFunc: function(d){ - return d.data.key + ' (' + d.data.values.length + ')'; - } - }) - } else if (type == "horizontal") { - console.log("Barchart horizontal") - html = bakeTheBarChartHorizontal({ - data: data, - barClass: 'cluster-bar', - barLabel: n, - width: options.width ? options.width : 70, - height: options.height ? options.height : 40, - barLabelClass: 'clustermarker-cluster-bar-label', - pathClassFunc: function(d){ - return "category-" + d.key; - }, - pathTitleFunc: function(d){ - return d.key + ' (' + d.values.length + ')'; - } + if (type == "custom") { + + // Step 1: Define a flexible aggregation function + function aggregateData(data, categoryField, aggregationFunc, valueField) { + return d3.nest() + .key(function(d) { return d.feature.properties[categoryField]; }) + .rollup(function(leaves) { + return aggregationFunc(leaves, function(d) { return d.feature.properties[valueField]; }); + }) + .entries(data); + } + + // Example aggregation functions + const aggregationFunctions = { + sum: (leaves, accessor) => d3.sum(leaves, accessor), + max: (leaves, accessor) => d3.max(leaves, accessor), + min: (leaves, accessor) => d3.min(leaves, accessor), + mean: (leaves, accessor) => d3.mean(leaves, accessor), + variance: (leaves, accessor) => d3.variance(leaves, accessor), + // Add more as needed + }; + + console.log("children"); console.log(children) + console.log("categoryField"); console.log(categoryField) + var data = aggregateData(children, categoryField, aggregationFunctions[aggregation], 'tosum'); + console.log("data"); console.log(data) + + console.log("Bubble chart"); + html = bakeTheBubbleChart({ + data: data, + valueFunc: function(d) { + var res = d.values; + console.log("valueFunc res"); console.log(res) + return res; + }, + outerRadius: r, + innerRadius: r-innerRadius, + bubbleClass: 'cluster-bubble', + bubbleLabelClass: 'clustermarker-cluster-bubble-label', + pathClassFunc: function(d) { + console.log('"category-" + d.key;'); console.log("category-" + d.key) + return "category-" + d.data.key; + }, + pathTitleFunc: function(d) { + console.log('pathTitleFunc'); console.log(d.key + ' (' + d.value + ')') + return d.data.key + ' (' + d.value + ')'; + } }); + } else { - console.log("Barchart") - html = bakeTheBarChart({ - data: data, - barClass: 'cluster-bar', - barLabel: n, - width: options.width ? options.width : 70, - height: options.height ? options.height : 40, - barLabelClass: 'clustermarker-cluster-bar-label', - pathClassFunc: function(d){ - return "category-" + d.key; - }, - pathTitleFunc: function(d){ - return d.key + ' (' + d.values.length + ')'; - } - }); + console.log("data");console.log(data) + console.log("categoryField");console.log(categoryField) + var data = d3.nest() //Build a dataset for the pie chart + .key(function(d) { return d.feature.properties[categoryField]; }) + .entries(children, d3.map) + + if (type == "pie") { + console.log("Piechart") + + html = bakeThePie({ + data: data, + valueFunc: function(d){return d.values.length;}, + outerRadius: r, + innerRadius: r-innerRadius, + pieClass: 'cluster-pie', + pieLabel: n, + pieLabelClass: 'clustermarker-cluster-pie-label', + pathClassFunc: function(d){ + return "category-" + d.data.key; + }, + pathTitleFunc: function(d){ + return d.data.key + ' (' + d.data.values.length + ')'; + } + }) + } else if (type == "horizontal") { + console.log("Barchart horizontal") + html = bakeTheBarChartHorizontal({ + data: data, + barClass: 'cluster-bar', + barLabel: n, + width: options.width ? options.width : 70, + height: options.height ? options.height : 40, + barLabelClass: 'clustermarker-cluster-bar-label', + pathClassFunc: function(d){ + return "category-" + d.key; + }, + pathTitleFunc: function(d){ + return d.key + ' (' + d.values.length + ')'; + } + }); + } else { + console.log("Barchart") + html = bakeTheBarChart({ + data: data, + barClass: 'cluster-bar', + barLabel: n, + width: options.width ? options.width : 70, + height: options.height ? options.height : 40, + barLabelClass: 'clustermarker-cluster-bar-label', + pathClassFunc: function(d){ + return "category-" + d.key; + }, + pathTitleFunc: function(d){ + return d.key + ' (' + d.values.length + ')'; + } + }); + } } //Create a new divIcon and assign the svg markup to the html property @@ -187,7 +244,116 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, return myIcon; } - //function that generates a svg markup for the Pie chart + //function that generates a svg markup for a Bubble chart + function bakeTheBubbleChart(options) { + if (!options.data || !options.valueFunc) { + return ''; + } + + if (false) { + console.log("1") + var data = options.data, + valueFunc = options.valueFunc, + r = options.outerRadius, + rInner = options.innerRadius, + pathClassFunc = options.pathClassFunc, + pathTitleFunc = options.pathTitleFunc, + origo = (r+strokeWidth), // Center coordinate + w = origo * 2, // Width and height of the svg element + h = w, + donut = d3.layout.pie(), + arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); + + console.log("3") + var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); + var vis = d3.select(svg) + .data([data]) + .attr('class', options.bubbleClass) + .attr('width', w) + .attr('height', h); + + console.log("4 - data"); + console.log(data) + var arcs = vis.selectAll('g.arc') + .data(donut.value(valueFunc)) + .enter().append('svg:g') + .attr('class', 'arc') + .attr('transform', 'translate(' + origo + ',' + origo + ')'); + + arcs.append('svg:path') + .attr('class', pathClassFunc) + .attr('stroke-width', strokeWidth) + .attr('d', arc) + + } else { + var data = options.data, + valueFunc = options.valueFunc, + r = options.outerRadius, + rInner = options.innerRadius, + pathClassFunc = options.pathClassFunc, + pathTitleFunc = options.pathTitleFunc, + bubbleLabelClass = options.bubbleLabelClass, + origo = (r+strokeWidth), // Center coordinate + w = origo * 2, // Width and height of the svg element + h = w, + donut = d3.layout.pie(), + arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); + + let radius = w; + + let pie = donut + .padAngle(1 / radius) + .sort(null) + .value(function(d) { return d.values; }); + + var arc = d3.svg.arc() + .innerRadius(rInner) + .outerRadius(r); + + //Create the pie chart + var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); + var vis = d3.select(svg) + .attr("width", w) + .attr("height", h) + + var arcs = vis.selectAll('g.arc') + .data(pie(data)) + .enter().append('svg:g') + .attr('class', 'arc') + .attr('transform', 'translate(' + origo + ',' + origo + ')'); + console.log("arcs"); console.log(arcs) + + arcs.append('svg:path') + .attr('class', pathClassFunc) + .attr('stroke-width', strokeWidth) + .attr('d', arc) + .append('svg:title') + .text(pathTitleFunc); + + // Text + vis.append('text') + .data(pie(data)) + //.data(data) + .attr('x',origo) + .attr('y',origo) + .attr('class', bubbleLabelClass) + .attr('text-anchor', 'middle') + .attr('fill', labelColor) + .attr('dy','.3em') + .text(function(d){ + console.log("TEXT - d"); console.log(d) + //return d.values; + return d.value; + }) + //.append('svg:title') + //.text(allTitles); + + } + + console.log("6") + return serializeXmlNode(svg); + } + //function that generates a svg markup for a Pie chart function bakeThePie(options) { //data and valueFunc are required if (!options.data || !options.valueFunc) { @@ -201,9 +367,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, pathClassFunc = options.pathClassFunc, pathTitleFunc = options.pathTitleFunc, pieClass = options.pieClass, - pieLabel = options.pieLabel?options.pieLabel:d3.sum(data,valueFunc), //Label for the whole pie + pieLabel = options.pieLabel ? options.pieLabel : d3.sum(data, valueFunc), //Label for the whole pie pieLabelClass = options.pieLabelClass, - origo = (r+strokeWidth), //Center coordinate w = origo*2, //width and height of the svg element h = w, @@ -287,7 +452,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, //Return the svg-markup rather than the actual element return serializeXmlNode(svg); } - //function that generates a svg markup for the Bar chart + //function that generates a svg markup for a Bar chart function bakeTheBarChart(options) { if (!options.data) { return ''; @@ -377,7 +542,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, return serializeXmlNode(svg); } - //function that generates a svg markup for the horizontal Bar chart + //function that generates a svg markup for a Bar chart (horizontal) function bakeTheBarChartHorizontal(options) { if (!options.data) { return ''; diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 21dde5e4..e0786727 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -10,10 +10,11 @@ addClusterCharts( lat = NULL, layerId = NULL, group = NULL, - type = c("pie", "bar", "horizontal"), + type = c("pie", "bar", "horizontal", "custom"), options = clusterchartOptions(), icon = NULL, html = NULL, + customFunc = NULL, popup = NULL, popupOptions = NULL, label = NULL, diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index 0b7550a8..fbb1fd57 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -16,6 +16,7 @@ clusterchartOptions( labelStroke = "black", labelColor = "black", labelOpacity = 0.9, + aggregation = "sum", sortTitlebyCount = TRUE ) } From 90b0a99dcba823864f4855b784cb00bd119aa758 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sat, 10 Aug 2024 09:44:30 +0200 Subject: [PATCH 14/26] feat: add totalaggregation per bubble --- .../lfx-clustercharts-bindings.js | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index fdbe28af..158d8d8b 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -153,6 +153,12 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var data = aggregateData(children, categoryField, aggregationFunctions[aggregation], 'tosum'); console.log("data"); console.log(data) + var totalAggregation = aggregationFunctions[aggregation](children, function(d) { + console.log("totalAggregation - d.feature.properties['tosum']"); console.log(d.feature.properties['tosum']) + return d.feature.properties['tosum']; + }); + console.log("Total Aggregation: " + totalAggregation); + console.log("Bubble chart"); html = bakeTheBubbleChart({ data: data, @@ -162,7 +168,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, return res; }, outerRadius: r, - innerRadius: r-innerRadius, + innerRadius: r - innerRadius, + totalAggregation: totalAggregation, bubbleClass: 'cluster-bubble', bubbleLabelClass: 'clustermarker-cluster-bubble-label', pathClassFunc: function(d) { @@ -292,6 +299,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, rInner = options.innerRadius, pathClassFunc = options.pathClassFunc, pathTitleFunc = options.pathTitleFunc, + totalAggregation = options.totalAggregation, bubbleLabelClass = options.bubbleLabelClass, origo = (r+strokeWidth), // Center coordinate w = origo * 2, // Width and height of the svg element @@ -331,11 +339,10 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .text(pathTitleFunc); // Text - vis.append('text') - .data(pie(data)) - //.data(data) - .attr('x',origo) - .attr('y',origo) + arcs.append('text') + .attr('transform', function(d) { + return 'translate(' + arc.centroid(d) + ')'; + }) .attr('class', bubbleLabelClass) .attr('text-anchor', 'middle') .attr('fill', labelColor) @@ -343,11 +350,20 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .text(function(d){ console.log("TEXT - d"); console.log(d) //return d.values; - return d.value; + return d.data.values; }) //.append('svg:title') //.text(allTitles); + vis.append('text') + .attr('x', origo) + .attr('y', origo) + .attr('class', bubbleLabelClass) + .attr('text-anchor', 'middle') + .attr('fill', labelColor) + .attr('dy', '.3em') + .text(totalAggregation); // Display the total aggregation + } console.log("6") From 2a8756e9f8faa4a416e72161148e0af82d32d11e Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Mon, 12 Aug 2024 09:47:49 +0200 Subject: [PATCH 15/26] feat: add valueField and digits, fix integer popup --- R/clusterCharts.R | 7 +- inst/examples/clustercharts_sum.R | 122 ++++++++++++ .../lfx-clustercharts-bindings.js | 178 +++++++++--------- man/addClusterCharts.Rd | 1 - man/clusterchartOptions.Rd | 2 + 5 files changed, 219 insertions(+), 91 deletions(-) create mode 100644 inst/examples/clustercharts_sum.R diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 3a25a228..67d742a4 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -73,7 +73,6 @@ addClusterCharts <- function( map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal","custom"), options = clusterchartOptions(), icon = NULL, html = NULL, - customFunc = NULL, popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, clusterOptions = NULL, clusterId = NULL, categoryField, categoryMap, popupFields = NULL, popupLabels = NULL, @@ -147,7 +146,7 @@ addClusterCharts <- function( map, NULL, "addClusterCharts", geojson, layerId, group, type, options, icon, html, popup, popupOptions, safeLabel(label, data), labelOptions, - clusterOptions, clusterId, customFunc, + clusterOptions, clusterId, categoryField, categoryMapList, popupFields, popupLabels, markerOptions, legendOptions ) %>% @@ -173,6 +172,8 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), labelColor = "black", labelOpacity = 0.9, aggregation = "sum", + valueField = NULL, + digits = 2, sortTitlebyCount = TRUE) { filterNULL(list( rmax = rmax @@ -187,6 +188,8 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), , labelColor = labelColor , labelOpacity = labelOpacity , aggregation = aggregation + , valueField = valueField + , digits = digits , sortTitlebyCount = sortTitlebyCount )) } diff --git a/inst/examples/clustercharts_sum.R b/inst/examples/clustercharts_sum.R new file mode 100644 index 00000000..e03c08e7 --- /dev/null +++ b/inst/examples/clustercharts_sum.R @@ -0,0 +1,122 @@ +library(shiny) +library(sf) +library(leaflet) +library(leaflet.extras) +library(leaflet.extras2) +options("shiny.autoreload" = TRUE) + + +# shipIcon <- leaflet::makeIcon( +# iconUrl = "./icons/Icon5.svg" +# ,className = "lsaicons" +# ,iconWidth = 24, iconHeight = 24, iconAnchorX = 0, iconAnchorY = 0 +# ) +shipIcon <- iconList( + "Schwer" = makeIcon("./icons/Icon5.svg", iconWidth = 32, iconHeight = 32), + "Mäßig" = makeIcon("./icons/Icon8.svg", iconWidth = 32, iconHeight = 32), + "Leicht" = makeIcon("./icons/Icon25.svg", iconWidth = 32, iconHeight = 32), + "kein Schaden" = makeIcon("./icons/Icon29.svg", iconWidth = 32, iconHeight = 32) +) +# shipIcon <- makeIcon( +# iconUrl = "https://cdn-icons-png.flaticon.com/512/1355/1355883.png", +# iconWidth = 40, iconHeight = 50, +# iconAnchorX = 0, iconAnchorY = 0 +# ) + +data <- sf::st_as_sf(breweries91) +data$category <- sample(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), size = nrow(data), replace = TRUE) +data$label <- paste0(data$brewery, "
", data$address) +data$id <- paste0("ID", seq.int(nrow(data))) +data$popup <- paste0("
", data$brewery, "
", data$address, "
") +data$tosum <- sample(1:100, nrow(data), replace = TRUE) +data$tosum2 <- sample(1:10, nrow(data), replace = TRUE) +# data$tosum <- 10 +data$tosumlabel <- paste("Sum: ", data$tosum) +data$web <- gsub(">(.*?)<", ">",data$tosum,"<", data$web) +data$web <- ifelse(is.na(data$web), "", paste0("
", data$web, "
")) + +ui <- fluidPage( + tags$head(tags$style(" + .clusterchartsicon { + background-position: left !important; + } + .markerhtml { + height: 100%; + margin-top: 8px; + left: 41px; + position: absolute; + }")), + leafletOutput("map", height = 650), + selectInput("aggr", "Aggregation", choices = c("sum","max", "min", "mean", + # "variance","deviation" ## working but not correct? ?? + # "cumsum", "mode", "least" ## not wokring - new d3 v? + "median"), selected = "mean"), + splitLayout(cellWidths = paste0(rep(20,4), "%"), + div(h5("Click Event"), verbatimTextOutput("click")), + div(h5("Mouseover Event"), verbatimTextOutput("mouseover")), + div(h5("Mouseout Event"), verbatimTextOutput("mouseout")), + div(h5("Drag-End Event"), verbatimTextOutput("dragend")) + ) +) + +server <- function(input, output, session) { + output$map <- renderLeaflet({ + leaflet() %>% addMapPane("clusterpane", 420) %>% + addProviderTiles("CartoDB") %>% + leaflet::addLayersControl(overlayGroups = c("clustermarkers","normalcircles")) %>% + # addCircleMarkers(data = data, group = "normalcircles", clusterOptions = markerClusterOptions()) %>% + addClusterCharts(data = data + , options = clusterchartOptions(rmax = 50, + size = 40, + # size = c(100,140), + labelBackground = TRUE, + labelOpacity = 0.5, + innerRadius = 20, + aggregation = input$aggr, + valueField = "tosum", + digits = 0, + sortTitlebyCount = TRUE) + # , type = "bar" + # , type = "horizontal" + , type = "custom" + , categoryField = "category" + , html = "web" + , icon = shipIcon + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("lightblue", "orange", "lightyellow", "lightgreen")) + , group = "clustermarkers" + , layerId = "id" + , clusterId = "id" + , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") + , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") + , label = "label" + ## Options ############# + , markerOptions = markerOptions(interactive = TRUE, + draggable = TRUE, + keyboard = TRUE, + title = "Some Marker Title", + zIndexOffset = 100, + opacity = 1, + riseOnHover = TRUE, + riseOffset = 400) + , legendOptions = list(position = "bottomright", title = "Unfälle im Jahr 2003") + , clusterOptions = markerClusterOptions(showCoverageOnHover = TRUE, + zoomToBoundsOnClick = TRUE, + spiderfyOnMaxZoom = TRUE, + removeOutsideVisibleBounds = TRUE, + spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5), + freezeAtZoom = TRUE, + clusterPane = "clusterpane", + spiderfyDistanceMultiplier = 2 + ) + , labelOptions = labelOptions(opacity = 0.8, textsize = "14px") + , popupOptions = popupOptions(maxWidth = 900, minWidth = 200, keepInView = TRUE) + ) + }) + output$click <- renderPrint({input$map_marker_click}) + output$mouseover <- renderPrint({input$map_marker_mouseover}) + output$mouseout <- renderPrint({input$map_marker_mouseout}) + output$dragend <- renderPrint({input$map_marker_dragend}) +} +shinyApp(ui, server) diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 158d8d8b..b58d67bc 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -2,7 +2,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, options, icon, html, popup, popupOptions, label, labelOptions, - clusterOptions, clusterId, customFunc, + clusterOptions, clusterId, categoryField, categoryMap, popupFields, popupLabels, markerOptions, legendOptions) { @@ -18,6 +18,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var labelOpacity = options.labelOpacity ? options.labelOpacity : 0.9; var sortTitlebyCount = options.sortTitlebyCount ? options.sortTitlebyCount : false; var aggregation = options.aggregation ? options.aggregation : "sum"; + var valueField = options.valueField ? options.valueField : null; + var digits = options.digits ? options.digits : null; // Make L.markerClusterGroup, markers, fitBounds and renderLegend console.log("geojson"); console.log(geojson) @@ -95,7 +97,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var props = feature.properties, popupContent = ''; if (popup && props[popup]) { - popupContent = props[popup]; + popupContent = props[popup] + ''; } else if (popupFields !== null ) { console.log("popupFields"); console.log(popupFields) popupContent += '
'; @@ -119,16 +121,16 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, } function defineClusterIcon(cluster) { var children = cluster.getAllChildMarkers(), - n = children.length //Get number of markers in cluster - var r = rmax-2*strokeWidth-(n<10?12:n<100?8:n<1000?4:0), //Calculate clusterpie radius... - iconDim = (r+strokeWidth)*2, //...and divIcon dimensions (leaflet really want to know the size) - html; - var innerRadius = options.innerRadius ? options.innerRadius : -10; + n = children.length, + r = rmax - 2 * strokeWidth - (n<10?12:n<100?8:n<1000?4:0), + iconDim = (r + strokeWidth) * 2, + html, + innerRadius = options.innerRadius ? options.innerRadius : -10; //bake some svg markup if (type == "custom") { - // Step 1: Define a flexible aggregation function + // Define aggregation function function aggregateData(data, categoryField, aggregationFunc, valueField) { return d3.nest() .key(function(d) { return d.feature.properties[categoryField]; }) @@ -138,46 +140,43 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .entries(data); } - // Example aggregation functions + // Aggregation functions const aggregationFunctions = { sum: (leaves, accessor) => d3.sum(leaves, accessor), max: (leaves, accessor) => d3.max(leaves, accessor), min: (leaves, accessor) => d3.min(leaves, accessor), mean: (leaves, accessor) => d3.mean(leaves, accessor), - variance: (leaves, accessor) => d3.variance(leaves, accessor), - // Add more as needed + median: (leaves, accessor) => d3.median(leaves, accessor), + //mode: (leaves, accessor) => d3.mode(leaves, accessor), + //cumsum: (leaves, accessor) => d3.cumsum(leaves, accessor), + //least: (leaves, accessor) => d3.least(leaves, accessor), + + //variance: (leaves, accessor) => d3.variance(leaves, accessor), + //deviation: (leaves, accessor) => d3.deviation(leaves, accessor), }; - console.log("children"); console.log(children) - console.log("categoryField"); console.log(categoryField) - var data = aggregateData(children, categoryField, aggregationFunctions[aggregation], 'tosum'); - console.log("data"); console.log(data) + // Aggregate Data based on the category `categoryField` and the value `valueField` + var data = aggregateData(children, categoryField, aggregationFunctions[aggregation], valueField); + // Calculate full aggregation for centered Text var totalAggregation = aggregationFunctions[aggregation](children, function(d) { - console.log("totalAggregation - d.feature.properties['tosum']"); console.log(d.feature.properties['tosum']) - return d.feature.properties['tosum']; + return d.feature.properties[valueField]; }); - console.log("Total Aggregation: " + totalAggregation); + totalAggregation = totalAggregation.toFixed(digits); - console.log("Bubble chart"); + // Make Chart html = bakeTheBubbleChart({ data: data, - valueFunc: function(d) { - var res = d.values; - console.log("valueFunc res"); console.log(res) - return res; - }, + valueFunc: function(d) { return d.values; }, outerRadius: r, innerRadius: r - innerRadius, totalAggregation: totalAggregation, bubbleClass: 'cluster-bubble', bubbleLabelClass: 'clustermarker-cluster-bubble-label', pathClassFunc: function(d) { - console.log('"category-" + d.key;'); console.log("category-" + d.key) return "category-" + d.data.key; }, pathTitleFunc: function(d) { - console.log('pathTitleFunc'); console.log(d.key + ' (' + d.value + ')') return d.data.key + ' (' + d.value + ')'; } }); @@ -194,16 +193,16 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, html = bakeThePie({ data: data, - valueFunc: function(d){return d.values.length;}, + valueFunc: function(d) { return d.values.length; }, outerRadius: r, - innerRadius: r-innerRadius, - pieClass: 'cluster-pie', + innerRadius: r - innerRadius, pieLabel: n, + pieClass: 'cluster-pie', pieLabelClass: 'clustermarker-cluster-pie-label', - pathClassFunc: function(d){ + pathClassFunc: function(d) { return "category-" + d.data.key; }, - pathTitleFunc: function(d){ + pathTitleFunc: function(d) { return d.data.key + ' (' + d.data.values.length + ')'; } }) @@ -211,10 +210,10 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, console.log("Barchart horizontal") html = bakeTheBarChartHorizontal({ data: data, - barClass: 'cluster-bar', - barLabel: n, width: options.width ? options.width : 70, height: options.height ? options.height : 40, + barLabel: n, + barClass: 'cluster-bar', barLabelClass: 'clustermarker-cluster-bar-label', pathClassFunc: function(d){ return "category-" + d.key; @@ -227,15 +226,15 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, console.log("Barchart") html = bakeTheBarChart({ data: data, - barClass: 'cluster-bar', - barLabel: n, width: options.width ? options.width : 70, height: options.height ? options.height : 40, + barLabel: n, + barClass: 'cluster-bar', barLabelClass: 'clustermarker-cluster-bar-label', - pathClassFunc: function(d){ + pathClassFunc: function(d) { return "category-" + d.key; }, - pathTitleFunc: function(d){ + pathTitleFunc: function(d) { return d.key + ' (' + d.values.length + ')'; } }); @@ -256,7 +255,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (!options.data || !options.valueFunc) { return ''; } - + console.log("bakeTheBubbleChart with these options"); console.log(options) if (false) { console.log("1") var data = options.data, @@ -301,8 +300,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, pathTitleFunc = options.pathTitleFunc, totalAggregation = options.totalAggregation, bubbleLabelClass = options.bubbleLabelClass, - origo = (r+strokeWidth), // Center coordinate - w = origo * 2, // Width and height of the svg element + origo = (r+strokeWidth), + w = origo * 2, h = w, donut = d3.layout.pie(), arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); @@ -311,34 +310,32 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, let pie = donut .padAngle(1 / radius) - .sort(null) - .value(function(d) { return d.values; }); + //.sort(null) + .value(valueFunc); var arc = d3.svg.arc() .innerRadius(rInner) .outerRadius(r); - //Create the pie chart + // Create SVG var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); var vis = d3.select(svg) .attr("width", w) .attr("height", h) + // Create the Arcs var arcs = vis.selectAll('g.arc') .data(pie(data)) .enter().append('svg:g') .attr('class', 'arc') .attr('transform', 'translate(' + origo + ',' + origo + ')'); - console.log("arcs"); console.log(arcs) arcs.append('svg:path') .attr('class', pathClassFunc) .attr('stroke-width', strokeWidth) .attr('d', arc) - .append('svg:title') - .text(pathTitleFunc); - // Text + // Display the text of each Arc arcs.append('text') .attr('transform', function(d) { return 'translate(' + arc.centroid(d) + ')'; @@ -347,14 +344,22 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('text-anchor', 'middle') .attr('fill', labelColor) .attr('dy','.3em') - .text(function(d){ - console.log("TEXT - d"); console.log(d) - //return d.values; - return d.data.values; - }) - //.append('svg:title') - //.text(allTitles); + .text(function(d){ return d.data.values.toFixed(digits); }) + .append('svg:title') + .text(pathTitleFunc); + // Show Label Background + if (labelBackground && labelBackground == true) { + vis.append('circle') + .attr('r', rInner-5) + .attr('transform', 'translate(' + origo + ',' + origo + ')') + .attr('fill', labelFill) + .attr('stroke', labelStroke) + .attr('stroke-width', strokeWidth) + .attr('opacity', labelOpacity) + } + + // Display the total aggregation in the Center vis.append('text') .attr('x', origo) .attr('y', origo) @@ -362,16 +367,13 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('text-anchor', 'middle') .attr('fill', labelColor) .attr('dy', '.3em') - .text(totalAggregation); // Display the total aggregation - + .text(totalAggregation); } - console.log("6") return serializeXmlNode(svg); } //function that generates a svg markup for a Pie chart function bakeThePie(options) { - //data and valueFunc are required if (!options.data || !options.valueFunc) { return ''; } @@ -383,24 +385,23 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, pathClassFunc = options.pathClassFunc, pathTitleFunc = options.pathTitleFunc, pieClass = options.pieClass, - pieLabel = options.pieLabel ? options.pieLabel : d3.sum(data, valueFunc), //Label for the whole pie + pieLabel = options.pieLabel ? options.pieLabel : d3.sum(data, valueFunc), pieLabelClass = options.pieLabelClass, - origo = (r+strokeWidth), //Center coordinate - w = origo*2, //width and height of the svg element + origo = (r + strokeWidth), + w = origo * 2, h = w, donut = d3.layout.pie(), arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); - //Create an svg element + // Create SVG var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); - //Create the pie chart var vis = d3.select(svg) .data([data]) .attr('class', pieClass) .attr('width', w) .attr('height', h); - // Arcs + // Create the Arcs var arcs = vis.selectAll('g.arc') .data(donut.value(valueFunc)) .enter().append('svg:g') @@ -415,7 +416,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .text(pathTitleFunc); // Create Title for Individual Elements and All in Cluster - console.log("d3pie data") pathTitleFunc = function(d){ return d.key + ' (' + d.values.length + ')'; } @@ -451,8 +451,9 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('stroke-width', strokeWidth) .attr('opacity', labelOpacity) .append('svg:title') - .text(allTitles); // Title for the rectangle with all values + .text(allTitles); } + // Text vis.append('text') .attr('x',origo) @@ -488,13 +489,14 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, x.domain(data.map(function(d) { return d.key; })); y.domain([0, d3.max(data, function(d) { return d.values.length; })]); + // Create svg var svg = document.createElementNS(d3.ns.prefix.svg, "svg"); var vis = d3.select(svg) .attr('class', barClass) .attr('width', width + strokeWidth) .attr('height', height + 20 + strokeWidth); - // Bars + // Create Bars vis.selectAll('.bar') .data(data) .enter().append('rect') @@ -532,29 +534,29 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Bar Label Background if (labelBackground && labelBackground == true) { vis.append('rect') - .attr('x', (width - (width - 10)) / 2) // Adjust the width of the background - .attr('y', height + 5) // Adjust the y position for the background - .attr('width', width - 10) // Width of the background - .attr('height', 15) // Height of the background + .attr('x', (width - (width - 10)) / 2) + .attr('y', height + 5) + .attr('width', width - 10) + .attr('height', 15) .attr('fill', labelFill) .attr('stroke', labelStroke) .attr('opacity', labelOpacity) .attr('stroke-width', strokeWidth) .append('svg:title') - .text(allTitles); // Title for the rectangle with all values + .text(allTitles); } - // Bar Label + // Bar Label (below) vis.append('text') .attr('x', width / 2) - .attr('y', height + 13) // Adjust the y position for the text + .attr('y', height + 13) .attr('class', barLabelClass) .attr('text-anchor', 'middle') .attr('dy', '.3em') .attr('fill', labelColor) .text(barLabel) .append('svg:title') - .text(allTitles); // Title for the rectangle with all values + .text(allTitles); return serializeXmlNode(svg); } @@ -572,33 +574,33 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, height = options.height, pathClassFunc = options.pathClassFunc, pathTitleFunc = options.pathTitleFunc, - x = d3.scale.linear().range([0, width]), // Linear scale for horizontal length - y = d3.scale.ordinal().rangeRoundBands([0, height], 0.1); // Ordinal scale for vertical positioning + x = d3.scale.linear().range([0, width]), + y = d3.scale.ordinal().rangeRoundBands([0, height], 0.1); x.domain([0, d3.max(data, function(d) { return d.values.length; })]); y.domain(data.map(function(d) { return d.key; })); + // Create SVG var svg = document.createElementNS(d3.ns.prefix.svg, "svg"); var vis = d3.select(svg) .attr('class', barClass) .attr('width', width + strokeWidth) .attr('height', height + 20 + strokeWidth); - // Bars + // Create Bars vis.selectAll('.bar') .data(data) .enter().append('rect') .attr('class', pathClassFunc) .attr('x', 0) .attr('y', function(d) { return y(d.key); }) - .attr('width', function(d) { return x(d.values.length); }) // Bar length based on x scale - .attr('height', y.rangeBand()) // Bar thickness based on y scale + .attr('width', function(d) { return x(d.values.length); }) + .attr('height', y.rangeBand()) .append('svg:title') .text(pathTitleFunc); // Create Title for Individual Elements and All in Cluster let allTitles = "" - //var allTitles = data.map(function(d) { return pathTitleFunc(d); }).join('\n'); if (sortTitlebyCount) { allTitles = data .sort(function(a, b) { return b.values.length - a.values.length; }) // Sort by length in descending order @@ -623,22 +625,22 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Bar Label Background if (labelBackground && labelBackground == true) { vis.append('rect') - .attr('x', 0) // Adjust the width of the background - .attr('y', height + 3) // Adjust the y position for the background - .attr('width', width) // Width of the background - .attr('height', 15) // Height of the background + .attr('x', 0) + .attr('y', height + 3) + .attr('width', width) + .attr('height', 15) .attr('fill', labelFill) .attr('stroke', labelStroke) .attr('opacity', labelOpacity) .attr('stroke-width', strokeWidth) .append('svg:title') - .text(allTitles); // Title for the rectangle with all values + .text(allTitles); } // Bar Label vis.append('text') .attr('x', width / 2) - .attr('y', (height + 8)) // Adjust the y position for the text + .attr('y', (height + 8)) .attr('class', barLabelClass) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') @@ -647,7 +649,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('fill', labelColor) .text(barLabel) .append('svg:title') - .text(allTitles); // Title for the rectangle with all values + .text(allTitles); return serializeXmlNode(svg); } diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index e0786727..798b299f 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -14,7 +14,6 @@ addClusterCharts( options = clusterchartOptions(), icon = NULL, html = NULL, - customFunc = NULL, popup = NULL, popupOptions = NULL, label = NULL, diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index fbb1fd57..12155d48 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -17,6 +17,8 @@ clusterchartOptions( labelColor = "black", labelOpacity = 0.9, aggregation = "sum", + valueField = NULL, + digits = 2, sortTitlebyCount = TRUE ) } From ae09c301a9207d8685a4324cb79e39756c40a4b1 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Mon, 12 Aug 2024 11:20:21 +0200 Subject: [PATCH 16/26] fix build warnings/errors, add docu --- DESCRIPTION | 5 +++-- NAMESPACE | 1 + R/clusterCharts.R | 23 ++++++++++++++----- R/leaflet.extras2-package.R | 1 + inst/examples/clustercharts_sum.R | 2 ++ man/addClusterCharts.Rd | 12 +++------- man/clusterchartOptions.Rd | 24 +++++++++++++++++--- tests/testthat/test-heightgraph.R | 1 - tests/testthat/test-movingmarker.R | 36 +++++++++++++++++------------- 9 files changed, 70 insertions(+), 35 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index dc781a3c..31a58752 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,9 +27,10 @@ Suggests: testthat (>= 2.1.0), fontawesome, htmlwidgets, + grDevices, + xfun, covr, - curl, - rmarkdown + curl URL: https://trafficonese.github.io/leaflet.extras2/, https://github.com/trafficonese/leaflet.extras2 BugReports: https://github.com/trafficonese/leaflet.extras2/issues RoxygenNote: 7.3.1 diff --git a/NAMESPACE b/NAMESPACE index 2904cabb..ee28a849 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -98,6 +98,7 @@ export(unsync) export(updateHexbin) export(velocityOptions) import(leaflet) +importFrom(grDevices,colorRampPalette) importFrom(htmltools,htmlDependency) importFrom(htmltools,tagGetAttribute) importFrom(htmltools,tagList) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 67d742a4..5f954c10 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -18,9 +18,11 @@ clusterchartsDependencies <- function() { #' addClusterCharts #' @description Adds cluster charts (either pie or bar charts) to a Leaflet map. -#' @param type The type of chart to use for clusters, either "pie" or "bar". +#' @param type The type of chart to use for clusters, either \code{c("pie","bar","horizontal","custom")}. #' @param categoryField The name of the feature property used to categorize the charts. #' @param categoryMap A data frame mapping categories to chart properties (label, color, icons, stroke). +#' @param icon Include an icon or a set of icons with \code{makeIcon} or \code{iconList} +#' @param html The html to include in the markers #' @param popup Use the column name given in popup to collect the feature property with this name. #' @param popupFields A string or vector of strings indicating the feature properties to include in popups. #' @param popupLabels A string or vector of strings indicating the labels for the popup fields. @@ -70,7 +72,8 @@ clusterchartsDependencies <- function() { #' , label = "brewery" #' ) addClusterCharts <- function( - map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal","custom"), + map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, + type = c("pie","bar","horizontal","custom"), options = clusterchartOptions(), icon = NULL, html = NULL, popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, @@ -158,9 +161,19 @@ addClusterCharts <- function( #' @param rmax The maximum radius of the clusters. #' @param size The size of the cluster markers. #' @param strokeWidth The stroke width in the chart. -#' @param width The width of the bar-chart. -#' @param height The height of the bar-chart. -#' @param innerRadius The inner radius of the pie-chart. +#' @param width The width of the bar-charts. +#' @param height The height of the bar-charts. +#' @param innerRadius The inner radius of the pie-charts. +#' @param labelBackground Should the label have a background? Default is `FALSE` +#' @param labelFill The label background color. Default is `white` +#' @param labelStroke The label stroke color. Default is `black` +#' @param labelColor The label color. Default is `black` +#' @param labelOpacity The label color. Default is `0.9` +#' @param aggregation If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used. +#' @param valueField If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column. +#' @param digits The amount of digits. Default is `2` +#' @param sortTitlebyCount Should the svg-title be sorted by count or by the categories. +#' #' @export clusterchartOptions <- function(rmax = 30, size = c(20, 20), width = 40, height = 50, diff --git a/R/leaflet.extras2-package.R b/R/leaflet.extras2-package.R index aca3ebdb..edb41e8c 100644 --- a/R/leaflet.extras2-package.R +++ b/R/leaflet.extras2-package.R @@ -7,6 +7,7 @@ #' @import leaflet #' @importFrom htmltools htmlDependency tagGetAttribute tags tagList #' @importFrom utils globalVariables adist packageVersion +#' @importFrom grDevices colorRampPalette ## usethis namespace: end NULL diff --git a/inst/examples/clustercharts_sum.R b/inst/examples/clustercharts_sum.R index e03c08e7..130e93b8 100644 --- a/inst/examples/clustercharts_sum.R +++ b/inst/examples/clustercharts_sum.R @@ -70,6 +70,8 @@ server <- function(input, output, session) { size = 40, # size = c(100,140), labelBackground = TRUE, + labelStroke = "orange", + labelColor = "gray", labelOpacity = 0.5, innerRadius = 20, aggregation = input$aggr, diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 798b299f..a4bcb4bc 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -50,19 +50,13 @@ Human-friendly group names are permitted--they need not be short, identifier-style names. Any number of layers and even different types of layers (e.g. markers and polygons) can share the same group name.} -\item{type}{The type of chart to use for clusters, either "pie" or "bar".} +\item{type}{The type of chart to use for clusters, either \code{c("pie","bar","horizontal","custom")}.} \item{options}{Additional options for cluster charts (see \code{\link{clusterchartOptions}}).} -\item{icon}{the icon(s) for markers; an icon is represented by an R list of -the form \code{list(iconUrl = "?", iconSize = c(x, y))}, and you can use -\code{\link[leaflet]{icons}()} to create multiple icons; note when you use an R list -that contains images as local files, these local image files will be base64 -encoded into the HTML page so the icon images will still be available even -when you publish the map elsewhere} +\item{icon}{Include an icon or a set of icons with \code{makeIcon} or \code{iconList}} -\item{html}{the content of the control. May be provided as string or as HTML -generated with Shiny/htmltools tags} +\item{html}{The html to include in the markers} \item{popup}{Use the column name given in popup to collect the feature property with this name.} diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index 12155d48..6894ba82 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -27,13 +27,31 @@ clusterchartOptions( \item{size}{The size of the cluster markers.} -\item{width}{The width of the bar-chart.} +\item{width}{The width of the bar-charts.} -\item{height}{The height of the bar-chart.} +\item{height}{The height of the bar-charts.} \item{strokeWidth}{The stroke width in the chart.} -\item{innerRadius}{The inner radius of the pie-chart.} +\item{innerRadius}{The inner radius of the pie-charts.} + +\item{labelBackground}{Should the label have a background? Default is `FALSE`} + +\item{labelFill}{The label background color. Default is `white`} + +\item{labelStroke}{The label stroke color. Default is `black`} + +\item{labelColor}{The label color. Default is `black`} + +\item{labelOpacity}{The label color. Default is `0.9`} + +\item{aggregation}{If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used.} + +\item{valueField}{If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column.} + +\item{digits}{The amount of digits. Default is `2`} + +\item{sortTitlebyCount}{Should the svg-title be sorted by count or by the categories.} } \description{ Adds options for clusterCharts diff --git a/tests/testthat/test-heightgraph.R b/tests/testthat/test-heightgraph.R index 1118760b..e3b3da34 100644 --- a/tests/testthat/test-heightgraph.R +++ b/tests/testthat/test-heightgraph.R @@ -28,7 +28,6 @@ data <- structure(list( ## TESTS ####################### test_that("heightgraph", { library(sf) - library(geojsonsf) data <- st_transform(data, 4326) data <- data.frame(st_coordinates(data)) diff --git a/tests/testthat/test-movingmarker.R b/tests/testthat/test-movingmarker.R index d48f4dec..0bfdccfe 100644 --- a/tests/testthat/test-movingmarker.R +++ b/tests/testthat/test-movingmarker.R @@ -17,11 +17,12 @@ df <- new("SpatialLinesDataFrame", data = structure(list(Name = structure(1L, le test_that("movingmarker", { - m <- leaflet() %>% + m <- expect_warning( + leaflet() %>% addMovingMarker(data = df, movingOptions = movingMarkerOptions(autostart = TRUE, loop = TRUE), label="I am a pirate!", - popup="Arrr") + popup="Arrr")) expect_is(m, "leaflet") expect_equal(m$x$calls[[1]]$method, "addMovingMarker") @@ -52,11 +53,13 @@ test_that("movingmarker", { ## Data is Simple Feature LINESTRING df1 <- sf::st_as_sf(df) - m <- leaflet() %>% + m <- expect_warning( + leaflet() %>% addMovingMarker(data = df1, movingOptions = movingMarkerOptions(autostart = TRUE, loop = TRUE), label="I am a pirate!", popup="Arrr") + ) expect_is(m, "leaflet") expect_equal(m$x$calls[[1]]$method, "addMovingMarker") deps <- findDependencies(m) @@ -64,49 +67,52 @@ test_that("movingmarker", { ## Data is Simple Feature POINT df1 <- sf::st_as_sf(df)[1,] - df1 <- sf::st_cast(df1, "POINT") - m <- leaflet() %>% + df1 <- expect_warning(sf::st_cast(df1, "POINT")) + m <- expect_warning( + leaflet() %>% addMovingMarker(data = df1, movingOptions = movingMarkerOptions(autostart = TRUE, loop = TRUE), label="I am a pirate!", popup="Arrr") + ) expect_is(m, "leaflet") expect_equal(m$x$calls[[1]]$method, "addMovingMarker") - + dfsf <- sf::st_as_sf(df) + dfsf <- expect_warning(st_cast(dfsf, "POINT")) + dfsf <- st_transform(dfsf, 4326) m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% startMoving() expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "startMoving") m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% startMoving() expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "startMoving") m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% stopMoving() expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "stopMoving") m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% pauseMoving() expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "pauseMoving") m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% resumeMoving() expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "resumeMoving") - m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% addLatLngMoving(latlng = list(33, -67), duration = 2000) expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addLatLngMoving") @@ -114,7 +120,7 @@ test_that("movingmarker", { expect_equal(m$x$calls[[length(m$x$calls)]]$args[[3]], 2000) m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% moveToMoving(latlng = list(33, -67), duration = 2000) expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "moveToMoving") @@ -122,7 +128,7 @@ test_that("movingmarker", { expect_equal(m$x$calls[[length(m$x$calls)]]$args[[3]], 2000) m <- leaflet() %>% - addMovingMarker(data = df) %>% + addMovingMarker(data = dfsf) %>% addStationMoving(pointIndex = 2, duration = 5000) expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addStationMoving") From 27c8adb6bda86d73bfdf26df38f7ba36a38f4a6e Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Mon, 12 Aug 2024 14:30:30 +0200 Subject: [PATCH 17/26] fix bugs, backround-repeat:round, add tests --- R/clusterCharts.R | 56 ++-- .../lfx-clustercharts-bindings.js | 2 +- man/clusterchartOptions.Rd | 4 +- tests/testthat/test-clustercharts.R | 277 ++++++++++++++++++ tests/testthat/test-contextmenu.R | 8 + tests/testthat/test-movingmarker.R | 16 +- tests/testthat/test-timeslider.R | 24 ++ 7 files changed, 347 insertions(+), 40 deletions(-) create mode 100644 tests/testthat/test-clustercharts.R diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 5f954c10..c3de081a 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -136,7 +136,7 @@ addClusterCharts <- function( clusterchartsDependencies()) ## Make Geojson ########### - if (!inherits(sf::st_as_sf(data), "sf")) { + if (!inherits(data, "sf")) { data <- sf::st_as_sf(data) } geojson <- yyjsonr::write_geojson_str(data) @@ -160,10 +160,10 @@ addClusterCharts <- function( #' @description Adds options for clusterCharts #' @param rmax The maximum radius of the clusters. #' @param size The size of the cluster markers. -#' @param strokeWidth The stroke width in the chart. +#' @param strokeWidth The stroke width of the chart. #' @param width The width of the bar-charts. #' @param height The height of the bar-charts. -#' @param innerRadius The inner radius of the pie-charts. +#' @param innerRadius The inner radius of pie-charts. #' @param labelBackground Should the label have a background? Default is `FALSE` #' @param labelFill The label background color. Default is `white` #' @param labelStroke The label stroke color. Default is `black` @@ -212,8 +212,7 @@ generate_css <- function(row, icon) { label <- row["labels"] color <- row["colors"] stroke <- row["strokes"] - if (is.null(color)) color <- stroke - if (is.null(stroke)) stroke <- color + if (is.null(stroke) || is.na(stroke)) stroke <- color ## Replace spaces with dots in the class name ####### label_nospaces <- gsub(" ", ".", label, fixed = TRUE) @@ -232,11 +231,11 @@ generate_css <- function(row, icon) { ## Make Icon ################ if (is.null(icon)) { icon <- row['icons'] - if (!is.null(icon)) { + if (!is.null(icon) && !is.na(icon)) { css <- paste0(css, ".icon-", label_nospaces, " {\n", " background-image: url('", icon, "');\n", - " background-repeat: no-repeat;\n", + " background-repeat: round;\n", " background-position: 0px 1px;\n", "}" ) @@ -264,13 +263,12 @@ generate_css <- function(row, icon) { css <- paste0(css, ".icon-", label_nospaces, " {\n", " background-image: url('", iconuse$data, "');\n", - " background-repeat: no-repeat;\n", + " background-repeat: round;\n", " background-position: 0px 1px;\n", size, "}" ) } - cat(css) css } @@ -279,26 +277,26 @@ b64EncodePackedIcons <- utils::getFromNamespace("b64EncodePackedIcons", "leaflet packStrings <- utils::getFromNamespace("packStrings", "leaflet") -backgroundCSS <- function(label, icon, - background_repeat = "no-repeat", - background_position = "0px 1px", - additional_css = list()) { - # Start the CSS string - css <- paste0(".icon-", label, " {\n", - " background-image: url('", icon, "');\n", - " background-repeat: ", background_repeat, ";\n", - " background-position: ", background_position, ";\n") - - # Add each additional CSS property - for (css_property in additional_css) { - css <- paste0(css, " ", css_property[1], ": ", css_property[2], ";\n") - } - - # Close the CSS block - css <- paste0(css, "}") - - return(css) -} +# backgroundCSS <- function(label, icon, +# background_repeat = "no-repeat", +# background_position = "0px 1px", +# additional_css = list()) { +# # Start the CSS string +# css <- paste0(".icon-", label, " {\n", +# " background-image: url('", icon, "');\n", +# " background-repeat: ", background_repeat, ";\n", +# " background-position: ", background_position, ";\n") +# +# # Add each additional CSS property +# for (css_property in additional_css) { +# css <- paste0(css, " ", css_property[1], ": ", css_property[2], ";\n") +# } +# +# # Close the CSS block +# css <- paste0(css, "}") +# +# return(css) +# } diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index b58d67bc..4bcae79a 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -672,7 +672,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, legendControl.onAdd = function(map) { var div = L.DomUtil.create('div', 'clusterlegend'); - div.innerHTML = '
' + legendOptions.title + '
'; + div.innerHTML = legendOptions.title ? '
' + legendOptions.title + '
' : ''; var legendItems = d3.select(div) .selectAll('.legenditem') diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index 6894ba82..446720d5 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -31,9 +31,9 @@ clusterchartOptions( \item{height}{The height of the bar-charts.} -\item{strokeWidth}{The stroke width in the chart.} +\item{strokeWidth}{The stroke width of the chart.} -\item{innerRadius}{The inner radius of the pie-charts.} +\item{innerRadius}{The inner radius of pie-charts.} \item{labelBackground}{Should the label have a background? Default is `FALSE`} diff --git a/tests/testthat/test-clustercharts.R b/tests/testthat/test-clustercharts.R new file mode 100644 index 00000000..cca1fe31 --- /dev/null +++ b/tests/testthat/test-clustercharts.R @@ -0,0 +1,277 @@ + +test_that("clustercharts", { + + # shipIcon <- leaflet::makeIcon( + # iconUrl = "./icons/Icon5.svg" + # ,className = "lsaicons" + # ,iconWidth = 24, iconHeight = 24, iconAnchorX = 0, iconAnchorY = 0 + # ) + + + ## data ########## + data <- sf::st_as_sf(breweries91) + data$category <- sample(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), size = nrow(data), replace = TRUE) + data$label <- paste0(data$brewery, "
", data$address) + data$id <- paste0("ID", seq.int(nrow(data))) + data$popup <- paste0("
", data$brewery, "
", data$address, "
") + data$tosum <- sample(1:100, nrow(data), replace = TRUE) + data$tosumlabel <- paste("Sum: ", data$tosum) + data$web <- gsub(">(.*?)<", ">",data$tosum,"<", data$web) + data$web <- ifelse(is.na(data$web), "", paste0("
", data$web, "
")) + + ## simple example ########## + m <- leaflet() %>% addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("lightblue", "orange", "lightyellow", "lightgreen"))) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## simple example (SP-data) ########## + m <- leaflet() %>% addProviderTiles("CartoDB") %>% + addClusterCharts(data = as(data, "Spatial") + , categoryField = "category" + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("lightblue", "orange", "lightyellow", "lightgreen"))) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## simple example with popupFields / popupLabels ########## + m <- leaflet() %>% addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("lightblue", "orange", "lightyellow", "lightgreen")) + , popupFields = c("id","brewery","address","zipcode", "category","tosum") + , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum") + ) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + m <- leaflet() %>% addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("lightblue", "orange", "lightyellow", "lightgreen")) + , popupFields = c("id","brewery","address","zipcode", "category","tosum") + ) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## No `categoryMap` - Error ########## + m <- expect_error( + leaflet() %>% + addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + )) + + ## No `categoryField` - Error ########## + m <- expect_error( + leaflet() %>% + addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryMap = + data.frame(colors = c("lightblue", "orange", "lightyellow", "lightgreen") + ) + )) + + ## No `colors` in `categoryMap` ########## + m <- expect_warning( + leaflet() %>% + addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden") + # ,colors = c("lightblue", "orange", "lightyellow", "lightgreen") + ) + )) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## No `labels` in `categoryMap` ########## + m <- expect_warning( + leaflet() %>% + addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , categoryMap = + data.frame(colors = c("lightblue", "orange", "lightyellow", "lightgreen") + ) + )) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## Multiple Sizes ########## + m <- leaflet() %>% + addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden") + ,colors = c("lightblue", "orange", "lightyellow", "lightgreen") + ) + , options = clusterchartOptions(size = c(10,40)) + ) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## Icons (single) ########## + shipIcon <- makeIcon( + iconUrl = "https://cdn-icons-png.flaticon.com/512/1355/1355883.png", + iconWidth = 40, iconHeight = 50, + iconAnchorX = 0, iconAnchorY = 0 + ) + m <- leaflet() %>% + addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , icon = shipIcon + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden") + ,colors = c("lightblue", "orange", "lightyellow", "lightgreen") + ) + , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") + , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") + , label = "label" + , options = clusterchartOptions(size = 50) + ) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## Icons (multiple) ########## + shipIcon <- iconList( + "Schwer" = makeIcon("https://leafletjs.com/examples/custom-icons/leaf-red.png", + iconWidth = 40, iconHeight = 50), + "Mäßig" = makeIcon("https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Maki2-ferry-18.svg/480px-Maki2-ferry-18.svg.png", + iconWidth = 40), + "Leicht" = makeIcon("https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Maki2-danger-24.svg/240px-Maki2-danger-24.svg.png", + iconWidth = 40), + "kein Schaden" = makeIcon("https://leafletjs.com/examples/custom-icons/leaf-green.png", + iconWidth = 40, iconHeight = 50) + ) + m <- leaflet() %>% + addProviderTiles("CartoDB") %>% + addClusterCharts(data = data + , categoryField = "category" + , icon = shipIcon + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden") + ,colors = c("lightblue", "orange", "lightyellow", "lightgreen") + ) + , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") + , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") + , label = "label" + , options = clusterchartOptions(size = c(30,35)) + ) + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## Icons in `categoryMap` ########## + iconvec <- c("https://leafletjs.com/examples/custom-icons/leaf-red.png", + "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Maki2-ferry-18.svg/480px-Maki2-ferry-18.svg.png", + "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Maki2-ferry-18.svg/480px-Maki2-ferry-18.svg.png", + "https://leafletjs.com/examples/custom-icons/leaf-green.png") + m <- leaflet() %>% addProviderTiles("CartoDB") %>% + addClusterCharts(data = as(data, "Spatial") + , categoryField = "category" + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("lightblue", "orange", "lightyellow", "lightgreen"), + icons = iconvec) + , options = clusterchartOptions(size = 50) + , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") + , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") + , label = "label") + deps <- findDependencies(m) + expect_equal(deps[[length(deps)]]$name, "lfx-clustercharts") + expect_equal(deps[[length(deps)-1]]$name, "leaflet-markercluster") + expect_equal(deps[[length(deps)-2]]$name, "lfx-clustercharts-css") + expect_equal(m$x$calls[[length(m$x$calls)]]$method, "addClusterCharts") + + ## ALL ############ + m <- leaflet() %>% addMapPane("clusterpane", 420) %>% + addClusterCharts(data = data + , options = clusterchartOptions(rmax = 50, + size = 40, + # size = c(100,140), + labelBackground = TRUE, + labelStroke = "orange", + labelColor = "gray", + labelOpacity = 0.5, + innerRadius = 20, + aggregation = "sum", + valueField = "tosum", + digits = 0, + sortTitlebyCount = TRUE) + # , type = "bar" + # , type = "horizontal" + # , type = "custom" + , categoryField = "category" + , html = "web" + , icon = shipIcon + , categoryMap = + data.frame(labels = c("Schwer", "Mäßig", "Leicht", "kein Schaden"), + colors = c("lightblue", "orange", "lightyellow", "lightgreen")) + , group = "clustermarkers" + , layerId = "id" + , clusterId = "id" + , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") + , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") + , label = "label" + , markerOptions = markerOptions(interactive = TRUE, + draggable = TRUE, + keyboard = TRUE, + title = "Some Marker Title", + zIndexOffset = 100, + opacity = 1, + riseOnHover = TRUE, + riseOffset = 400) + , legendOptions = list(position = "bottomright", title = "Unfälle im Jahr 2003") + , clusterOptions = markerClusterOptions(showCoverageOnHover = TRUE, + zoomToBoundsOnClick = TRUE, + spiderfyOnMaxZoom = TRUE, + removeOutsideVisibleBounds = TRUE, + spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5), + freezeAtZoom = TRUE, + clusterPane = "clusterpane", + spiderfyDistanceMultiplier = 2 + ) + , labelOptions = labelOptions(opacity = 0.8, textsize = "14px") + , popupOptions = popupOptions(maxWidth = 900, minWidth = 200, keepInView = TRUE) + ) + +}) diff --git a/tests/testthat/test-contextmenu.R b/tests/testthat/test-contextmenu.R index 97aa18f2..4cf4e97b 100644 --- a/tests/testthat/test-contextmenu.R +++ b/tests/testthat/test-contextmenu.R @@ -46,6 +46,14 @@ test_that("contextmenu", { expect_equal(m$x$calls[[length(m$x$calls)]]$method, "hideContextmenu") + m <- m %>% enableContextmenu() + expect_equal(m$x$calls[[length(m$x$calls)]]$method, + "enableContextmenu") + + m <- m %>% disableContextmenu() + expect_equal(m$x$calls[[length(m$x$calls)]]$method, + "disableContextmenu") + if (packageVersion("leaflet") < "2.0.4") { m <- expect_warning( m %>% addItemContextmenu( diff --git a/tests/testthat/test-movingmarker.R b/tests/testthat/test-movingmarker.R index 0bfdccfe..5abe7c7b 100644 --- a/tests/testthat/test-movingmarker.R +++ b/tests/testthat/test-movingmarker.R @@ -19,10 +19,10 @@ test_that("movingmarker", { m <- expect_warning( leaflet() %>% - addMovingMarker(data = df, - movingOptions = movingMarkerOptions(autostart = TRUE, loop = TRUE), - label="I am a pirate!", - popup="Arrr")) + addMovingMarker(data = df, + movingOptions = movingMarkerOptions(autostart = TRUE, loop = TRUE), + label="I am a pirate!", + popup="Arrr")) expect_is(m, "leaflet") expect_equal(m$x$calls[[1]]$method, "addMovingMarker") @@ -82,14 +82,14 @@ test_that("movingmarker", { dfsf <- expect_warning(st_cast(dfsf, "POINT")) dfsf <- st_transform(dfsf, 4326) m <- leaflet() %>% - addMovingMarker(data = dfsf) %>% - startMoving() + addMovingMarker(data = dfsf) %>% + startMoving() expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "startMoving") m <- leaflet() %>% - addMovingMarker(data = dfsf) %>% - startMoving() + addMovingMarker(data = dfsf) %>% + startMoving() expect_is(m, "leaflet") expect_equal(m$x$calls[[length(m$x$calls)]]$method, "startMoving") diff --git a/tests/testthat/test-timeslider.R b/tests/testthat/test-timeslider.R index 85c70f1f..323b12fd 100644 --- a/tests/testthat/test-timeslider.R +++ b/tests/testthat/test-timeslider.R @@ -49,4 +49,28 @@ test_that("timeslider", { removeTimeslider() expect_identical(m$x$calls[[length(m$x$calls)]]$method, "removeTimeslider") + m <- leaflet() %>% + addTimeslider(data = data, fill = FALSE, + label = ~Name, + options = timesliderOptions( + position = "topright", + timeAttribute = "time", + range = FALSE)) + expect_is(m, "leaflet") + expect_identical(m$x$calls[[1]]$method, "addTimeslider") + expect_is(m$x$calls[[1]]$args[[1]], "geojson") + expect_true(inherits(m$x$calls[[1]]$args[[1]], "geojson")) + + m <- leaflet() %>% + addTimeslider(data = data, fill = FALSE, + label = data$Name, + options = timesliderOptions( + position = "topright", + timeAttribute = "time", + range = FALSE)) + expect_is(m, "leaflet") + expect_identical(m$x$calls[[1]]$method, "addTimeslider") + expect_is(m$x$calls[[1]]$args[[1]], "geojson") + expect_true(inherits(m$x$calls[[1]]$args[[1]], "geojson")) + }) From 35d76ed23c9b0f8ecf48aca91d8a9e2af7e246a3 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Mon, 12 Aug 2024 23:05:09 +0200 Subject: [PATCH 18/26] added clustercharts News, pkgdown, refactor (move aggregation/valueFile out of options, cleanup --- NEWS.md | 1 + R/clusterCharts.R | 15 +- _pkgdown.yml | 5 + inst/examples/clusterCharts_app.R | 44 ++-- inst/examples/clustercharts_sum.R | 34 ++- .../lfx-clustercharts-bindings.js | 211 +++++++----------- man/addClusterCharts.Rd | 6 + man/clusterchartOptions.Rd | 6 - 8 files changed, 151 insertions(+), 171 deletions(-) diff --git a/NEWS.md b/NEWS.md index 102bed15..c35a1849 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,7 @@ * The opened sidebar tab is returned as Shiny input using the `sidebar_tabs` ID. * allow `...` in `antpathOptions` to be able to set the pane (e.g.: `renderer= JS('L.svg({pane: "my-pane"})')`) * Added custom `clusterCharts` using `Leaflet.markercluster` and `d3` for piechart and barcharts. +* Added `addClusterCharts` to enable **pie** and **bar** charts in Marker clusters using `Leaflet.markercluster`, `d3` and `L.DivIcon`, with support for customizable category styling and various aggregation methods like **sum, min, max, mean**, and **median**. * Switched from `geojsonsf` to `yyjsonr` (*heightgraph*, *timeslider*, *clustercharts*) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index c3de081a..33b25936 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -21,6 +21,8 @@ clusterchartsDependencies <- function() { #' @param type The type of chart to use for clusters, either \code{c("pie","bar","horizontal","custom")}. #' @param categoryField The name of the feature property used to categorize the charts. #' @param categoryMap A data frame mapping categories to chart properties (label, color, icons, stroke). +#' @param aggregation If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used. +#' @param valueField If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column. #' @param icon Include an icon or a set of icons with \code{makeIcon} or \code{iconList} #' @param html The html to include in the markers #' @param popup Use the column name given in popup to collect the feature property with this name. @@ -74,6 +76,7 @@ clusterchartsDependencies <- function() { addClusterCharts <- function( map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal","custom"), + aggregation = "sum", valueField = NULL, options = clusterchartOptions(), icon = NULL, html = NULL, popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, @@ -109,6 +112,8 @@ addClusterCharts <- function( clusterOptions$maxClusterRadius = NULL clusterOptions$iconCreateFunction = NULL } + options$aggregation = aggregation + options$valueField = valueField ## CSS string ############# css <- paste(apply(categoryMap, 1, generate_css, icon), collapse = "\n") @@ -169,8 +174,6 @@ addClusterCharts <- function( #' @param labelStroke The label stroke color. Default is `black` #' @param labelColor The label color. Default is `black` #' @param labelOpacity The label color. Default is `0.9` -#' @param aggregation If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used. -#' @param valueField If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column. #' @param digits The amount of digits. Default is `2` #' @param sortTitlebyCount Should the svg-title be sorted by count or by the categories. #' @@ -184,8 +187,6 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), labelStroke = "black", labelColor = "black", labelOpacity = 0.9, - aggregation = "sum", - valueField = NULL, digits = 2, sortTitlebyCount = TRUE) { filterNULL(list( @@ -200,8 +201,6 @@ clusterchartOptions <- function(rmax = 30, size = c(20, 20), , labelStroke = labelStroke , labelColor = labelColor , labelOpacity = labelOpacity - , aggregation = aggregation - , valueField = valueField , digits = digits , sortTitlebyCount = sortTitlebyCount )) @@ -235,7 +234,7 @@ generate_css <- function(row, icon) { css <- paste0(css, ".icon-", label_nospaces, " {\n", " background-image: url('", icon, "');\n", - " background-repeat: round;\n", + " background-repeat: no-repeat;\n", " background-position: 0px 1px;\n", "}" ) @@ -263,7 +262,7 @@ generate_css <- function(row, icon) { css <- paste0(css, ".icon-", label_nospaces, " {\n", " background-image: url('", iconuse$data, "');\n", - " background-repeat: round;\n", + " background-repeat: no-repeat;\n", " background-position: 0px 1px;\n", size, "}" diff --git a/_pkgdown.yml b/_pkgdown.yml index 7a18070c..6a6310f7 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -25,6 +25,11 @@ reference: - menuItem - mapmenuItems - markermenuItems + - title: Clustercharts with d3 + contents: + - matches("Contextmenu") + - matches("ClusterCharts") + - matches("clusterchart") - title: Easy Print contents: - matches("Easyprint") diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index 8a6acaf6..0826a27a 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -34,8 +34,12 @@ data$tosum <- sample(1:100, nrow(data), replace = TRUE) ui <- fluidPage( tags$head(tags$style(" - .clusterchartsicon { - background-position: left !important; + .inputs { + display: flex; + } + .inputdiv { + position: relative; + z-index: 100000000; } .markerhtml { height: 100%; @@ -43,6 +47,18 @@ ui <- fluidPage( left: 41px; position: absolute; }")), + div(class="inputdiv", + div(class="inputs", + selectInput("type", "Plot type", choices = c("bar","horizontal", "pie"), selected = "pie"), + numericInput("stroke", "strokeWidth", 1, 1, 10), + numericInput("rmax", "MaxRadius", 50, 1, 150), + numericInput("innerRadius", "InnerRadius", 10, 1, 100), + numericInput("width", "Width", 50, 1, 150), + numericInput("height", "Height", 50, 1, 150), + selectInput("labelBackground", "labelBackground", choices = c(T,F)), + selectInput("sortTitlebyCount", "sortTitlebyCount", choices = c(T,F)), + numericInput("labelOpacity", "labelOpacity", 0.5, 0, 1, step = 0.1), + )), leafletOutput("map", height = 650), splitLayout(cellWidths = paste0(rep(20,4), "%"), div(verbatimTextOutput("click")), @@ -59,20 +75,22 @@ server <- function(input, output, session) { leaflet::addLayersControl(overlayGroups = c("clustermarkers","normalcircles")) %>% # addCircleMarkers(data = data, group = "normalcircles", clusterOptions = markerClusterOptions()) %>% addClusterCharts(data = data - , options = clusterchartOptions(rmax = 50, + , options = clusterchartOptions(rmax = input$rmax, size = c(100,40), # size=40, - width = 100, height = 30, - strokeWidth = 1, - labelBackground = T, + width = input$width, + height = input$height, + strokeWidth = input$stroke, + labelBackground = as.logical(input$labelBackground), # labelFill = "red", # labelStroke = "green", labelColor = "blue", - labelOpacity = 0.5, - innerRadius = 10, - sortTitlebyCount = T) + labelOpacity = input$labelOpacity, + innerRadius = input$innerRadius, + sortTitlebyCount = as.logical(input$sortTitlebyCount)) # , type = "bar" # , type = "horizontal" + , type = input$type , categoryField = "categoryfields" , html = "web" , icon = shipIcon @@ -86,21 +104,19 @@ server <- function(input, output, session) { strokes = "gray" ) , group = "clustermarkers" - # group = "zipcode", , layerId = "id" , clusterId = "id" , popupFields = c("brewery","address","zipcode", "category") , popupLabels = c("Brauerei","Addresse","PLZ", "Art") - , popup = "popup" + # , popup = "popup" , label = "label" ## Options ############# , markerOptions = markerOptions(interactive = TRUE, draggable = TRUE, keyboard = TRUE, - # stroke = 0.1, title = "Some Marker Title", zIndexOffset = 100, - opacity = 1, + opacity = 0.6, riseOnHover = TRUE, riseOffset = 400) , legendOptions = list(position = "bottomright", title = "Unfälle im Jahr 2003") @@ -111,7 +127,7 @@ server <- function(input, output, session) { , spiderLegPolylineOptions = list(weight = 1.5, color = "#222", opacity = 0.5) , freezeAtZoom = TRUE , clusterPane = "clusterpane" - , spiderfyDistanceMultiplier = 1 + , spiderfyDistanceMultiplier = 34 ) , labelOptions = labelOptions(opacity = 0.8, textsize = "14px") , popupOptions = popupOptions(maxWidth = 900, minWidth = 200, keepInView = TRUE) diff --git a/inst/examples/clustercharts_sum.R b/inst/examples/clustercharts_sum.R index 130e93b8..eb8a144e 100644 --- a/inst/examples/clustercharts_sum.R +++ b/inst/examples/clustercharts_sum.R @@ -5,7 +5,7 @@ library(leaflet.extras) library(leaflet.extras2) options("shiny.autoreload" = TRUE) - +## Icons ############## # shipIcon <- leaflet::makeIcon( # iconUrl = "./icons/Icon5.svg" # ,className = "lsaicons" @@ -23,6 +23,7 @@ shipIcon <- iconList( # iconAnchorX = 0, iconAnchorY = 0 # ) +## Data ############## data <- sf::st_as_sf(breweries91) data$category <- sample(c("Schwer", "Mäßig", "Leicht", "kein Schaden"), size = nrow(data), replace = TRUE) data$label <- paste0(data$brewery, "
", data$address) @@ -30,16 +31,13 @@ data$id <- paste0("ID", seq.int(nrow(data))) data$popup <- paste0("
", data$brewery, "
", data$address, "
") data$tosum <- sample(1:100, nrow(data), replace = TRUE) data$tosum2 <- sample(1:10, nrow(data), replace = TRUE) -# data$tosum <- 10 data$tosumlabel <- paste("Sum: ", data$tosum) data$web <- gsub(">(.*?)<", ">",data$tosum,"<", data$web) data$web <- ifelse(is.na(data$web), "", paste0("
", data$web, "
")) +## UI ############## ui <- fluidPage( tags$head(tags$style(" - .clusterchartsicon { - background-position: left !important; - } .markerhtml { height: 100%; margin-top: 8px; @@ -47,10 +45,13 @@ ui <- fluidPage( position: absolute; }")), leafletOutput("map", height = 650), - selectInput("aggr", "Aggregation", choices = c("sum","max", "min", "mean", - # "variance","deviation" ## working but not correct? ?? - # "cumsum", "mode", "least" ## not wokring - new d3 v? - "median"), selected = "mean"), + selectInput("type", "Plot type", choices = c("bar","horizontal", "custom", "pie")), + conditionalPanel("input.type == 'custom'", + selectInput("aggr", "Aggregation", choices = c("sum","max", "min", "mean", + # "variance","deviation" ## working but not correct? ?? + # "cumsum", "mode", "least" ## not wokring - new d3 v? + "median"), selected = "mean") + ), splitLayout(cellWidths = paste0(rep(20,4), "%"), div(h5("Click Event"), verbatimTextOutput("click")), div(h5("Mouseover Event"), verbatimTextOutput("mouseover")), @@ -59,6 +60,7 @@ ui <- fluidPage( ) ) +## Server ############## server <- function(input, output, session) { output$map <- renderLeaflet({ leaflet() %>% addMapPane("clusterpane", 420) %>% @@ -70,17 +72,13 @@ server <- function(input, output, session) { size = 40, # size = c(100,140), labelBackground = TRUE, - labelStroke = "orange", - labelColor = "gray", labelOpacity = 0.5, innerRadius = 20, - aggregation = input$aggr, - valueField = "tosum", digits = 0, sortTitlebyCount = TRUE) - # , type = "bar" - # , type = "horizontal" - , type = "custom" + , aggregation = input$aggr + , valueField = "tosum" + , type = input$type , categoryField = "category" , html = "web" , icon = shipIcon @@ -90,8 +88,8 @@ server <- function(input, output, session) { , group = "clustermarkers" , layerId = "id" , clusterId = "id" - , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") - , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") + # , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") + # , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") , label = "label" ## Options ############# , markerOptions = markerOptions(interactive = TRUE, diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 4bcae79a..90f3261a 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -22,8 +22,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var digits = options.digits ? options.digits : null; // Make L.markerClusterGroup, markers, fitBounds and renderLegend - console.log("geojson"); console.log(geojson) - console.log("clusterOptions"); console.log(clusterOptions) var markerclusters = L.markerClusterGroup( Object.assign({ maxClusterRadius: 2 * rmax, @@ -54,7 +52,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, // Functions function defineFeature(feature, latlng) { var categoryVal = feature.properties[categoryField] - var myClass = 'clustermarker category-'+categoryVal+' clusterchartsicon icon-'+categoryVal; + var myClass = 'clustermarker category-'+categoryVal+' icon-'+categoryVal; let extraInfo = { clusterId: clusterId }; //console.log("feature"); console.log(feature) @@ -99,7 +97,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (popup && props[popup]) { popupContent = props[popup] + ''; } else if (popupFields !== null ) { - console.log("popupFields"); console.log(popupFields) popupContent += '
'; popupFields.map( function(key, idx) { if (props[key]) { @@ -150,7 +147,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, //mode: (leaves, accessor) => d3.mode(leaves, accessor), //cumsum: (leaves, accessor) => d3.cumsum(leaves, accessor), //least: (leaves, accessor) => d3.least(leaves, accessor), - //variance: (leaves, accessor) => d3.variance(leaves, accessor), //deviation: (leaves, accessor) => d3.deviation(leaves, accessor), }; @@ -182,15 +178,11 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, }); } else { - console.log("data");console.log(data) - console.log("categoryField");console.log(categoryField) var data = d3.nest() //Build a dataset for the pie chart .key(function(d) { return d.feature.properties[categoryField]; }) .entries(children, d3.map) if (type == "pie") { - console.log("Piechart") - html = bakeThePie({ data: data, valueFunc: function(d) { return d.values.length; }, @@ -207,7 +199,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, } }) } else if (type == "horizontal") { - console.log("Barchart horizontal") html = bakeTheBarChartHorizontal({ data: data, width: options.width ? options.width : 70, @@ -215,15 +206,14 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, barLabel: n, barClass: 'cluster-bar', barLabelClass: 'clustermarker-cluster-bar-label', - pathClassFunc: function(d){ + pathClassFunc: function(d) { return "category-" + d.key; }, - pathTitleFunc: function(d){ + pathTitleFunc: function(d) { return d.key + ' (' + d.values.length + ')'; } }); } else { - console.log("Barchart") html = bakeTheBarChart({ data: data, width: options.width ? options.width : 70, @@ -255,120 +245,85 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (!options.data || !options.valueFunc) { return ''; } - console.log("bakeTheBubbleChart with these options"); console.log(options) - if (false) { - console.log("1") - var data = options.data, - valueFunc = options.valueFunc, - r = options.outerRadius, - rInner = options.innerRadius, - pathClassFunc = options.pathClassFunc, - pathTitleFunc = options.pathTitleFunc, - origo = (r+strokeWidth), // Center coordinate - w = origo * 2, // Width and height of the svg element - h = w, - donut = d3.layout.pie(), - arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); - - console.log("3") - var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); - var vis = d3.select(svg) - .data([data]) - .attr('class', options.bubbleClass) - .attr('width', w) - .attr('height', h); - - console.log("4 - data"); - console.log(data) - var arcs = vis.selectAll('g.arc') - .data(donut.value(valueFunc)) - .enter().append('svg:g') - .attr('class', 'arc') - .attr('transform', 'translate(' + origo + ',' + origo + ')'); - - arcs.append('svg:path') - .attr('class', pathClassFunc) - .attr('stroke-width', strokeWidth) - .attr('d', arc) - - } else { - var data = options.data, - valueFunc = options.valueFunc, - r = options.outerRadius, - rInner = options.innerRadius, - pathClassFunc = options.pathClassFunc, - pathTitleFunc = options.pathTitleFunc, - totalAggregation = options.totalAggregation, - bubbleLabelClass = options.bubbleLabelClass, - origo = (r+strokeWidth), - w = origo * 2, - h = w, - donut = d3.layout.pie(), - arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); + var data = options.data, + valueFunc = options.valueFunc, + r = options.outerRadius, + rInner = options.innerRadius, + pathClassFunc = options.pathClassFunc, + pathTitleFunc = options.pathTitleFunc, + totalAggregation = options.totalAggregation, + bubbleLabelClass = options.bubbleLabelClass, + origo = (r + strokeWidth), + w = origo * 2, + h = w, + donut = d3.layout.pie(), + arc = d3.svg.arc().innerRadius(rInner).outerRadius(r); - let radius = w; + let radius = w; - let pie = donut - .padAngle(1 / radius) - //.sort(null) - .value(valueFunc); + let pie = donut + .padAngle(1 / radius) + //.sort(null) + .value(valueFunc); - var arc = d3.svg.arc() - .innerRadius(rInner) - .outerRadius(r); + var arc = d3.svg.arc() + .innerRadius(rInner) + .outerRadius(r); - // Create SVG - var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); - var vis = d3.select(svg) - .attr("width", w) - .attr("height", h) + // Create SVG + var svg = document.createElementNS(d3.ns.prefix.svg, 'svg'); + var vis = d3.select(svg) + .attr("width", w) + .attr("height", h) + + // Create the Arcs + var arcs = vis.selectAll('g.arc') + .data(pie(data)) + .enter().append('svg:g') + .attr('class', 'arc') + .attr('transform', 'translate(' + origo + ',' + origo + ')'); + + arcs.append('svg:path') + .attr('class', pathClassFunc) + .attr('stroke-width', strokeWidth) + .attr('d', arc) + + // Display the text of each Arc + arcs.append('text') + .attr('transform', function(d) { + return 'translate(' + arc.centroid(d) + ')'; + }) + .attr('class', bubbleLabelClass) + .attr('text-anchor', 'middle') + .attr('fill', labelColor) + .attr('dy','.3em') + .text(function(d){ return d.data.values.toFixed(digits); }) + .append('svg:title') + .text(pathTitleFunc); - // Create the Arcs - var arcs = vis.selectAll('g.arc') - .data(pie(data)) - .enter().append('svg:g') - .attr('class', 'arc') - .attr('transform', 'translate(' + origo + ',' + origo + ')'); + // Show Label Background + if (labelBackground && labelBackground == true) { + vis.append('circle') + .attr('r', rInner-5) + .attr('transform', 'translate(' + origo + ',' + origo + ')') + .attr('fill', labelFill) + .attr('stroke', labelStroke) + .attr('stroke-width', strokeWidth) + .attr('opacity', labelOpacity) + } - arcs.append('svg:path') - .attr('class', pathClassFunc) + // Display the total aggregation in the Center + vis.append('text') + .attr('x', origo) + .attr('y', origo) + .attr('class', bubbleLabelClass) + .attr('text-anchor', 'middle') + .attr('fill', labelColor) + .attr('stroke', labelStroke) + .attr('opacity', labelOpacity) .attr('stroke-width', strokeWidth) - .attr('d', arc) - - // Display the text of each Arc - arcs.append('text') - .attr('transform', function(d) { - return 'translate(' + arc.centroid(d) + ')'; - }) - .attr('class', bubbleLabelClass) - .attr('text-anchor', 'middle') - .attr('fill', labelColor) - .attr('dy','.3em') - .text(function(d){ return d.data.values.toFixed(digits); }) - .append('svg:title') - .text(pathTitleFunc); - - // Show Label Background - if (labelBackground && labelBackground == true) { - vis.append('circle') - .attr('r', rInner-5) - .attr('transform', 'translate(' + origo + ',' + origo + ')') - .attr('fill', labelFill) - .attr('stroke', labelStroke) - .attr('stroke-width', strokeWidth) - .attr('opacity', labelOpacity) - } - - // Display the total aggregation in the Center - vis.append('text') - .attr('x', origo) - .attr('y', origo) - .attr('class', bubbleLabelClass) - .attr('text-anchor', 'middle') - .attr('fill', labelColor) - .attr('dy', '.3em') - .text(totalAggregation); - } + .attr('dy', '.3em') + .text(totalAggregation); return serializeXmlNode(svg); } @@ -377,7 +332,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (!options.data || !options.valueFunc) { return ''; } - console.log("bakeThePie with these options"); console.log(options) var data = options.data, valueFunc = options.valueFunc, r = options.outerRadius, @@ -416,7 +370,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .text(pathTitleFunc); // Create Title for Individual Elements and All in Cluster - pathTitleFunc = function(d){ + pathTitleFunc = function(d) { return d.key + ' (' + d.values.length + ')'; } let allTitles = "" @@ -461,6 +415,9 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('class', pieLabelClass) .attr('text-anchor', 'middle') .attr('fill', labelColor) + .attr('stroke', labelStroke) + .attr('opacity', labelOpacity) + .attr('stroke-width', strokeWidth) .attr('dy','.3em') .text(pieLabel) .append('svg:title') @@ -474,7 +431,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (!options.data) { return ''; } - console.log("bakeTheBarChart with these options"); console.log(options) var data = options.data, barClass = options.barClass, barLabel = options.barLabel ? options.barLabel : d3.sum(data, function(d) { return d.values.length; }), @@ -552,8 +508,11 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('y', height + 13) .attr('class', barLabelClass) .attr('text-anchor', 'middle') - .attr('dy', '.3em') .attr('fill', labelColor) + .attr('stroke', labelStroke) + .attr('opacity', labelOpacity) + .attr('stroke-width', strokeWidth) + .attr('dy', '.3em') .text(barLabel) .append('svg:title') .text(allTitles); @@ -565,7 +524,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, if (!options.data) { return ''; } - console.log("bakeTheBarChart with these options"); console.log(options) var data = options.data, barClass = options.barClass, barLabel = options.barLabel ? options.barLabel : d3.sum(data, function(d) { return d.values.length; }), @@ -645,6 +603,9 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') .attr('alignment-baseline', 'middle') + .attr('stroke', labelStroke) + .attr('opacity', labelOpacity) + .attr('stroke-width', strokeWidth) .attr('dy', '.3em') .attr('fill', labelColor) .text(barLabel) diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index a4bcb4bc..18c0732f 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -11,6 +11,8 @@ addClusterCharts( layerId = NULL, group = NULL, type = c("pie", "bar", "horizontal", "custom"), + aggregation = "sum", + valueField = NULL, options = clusterchartOptions(), icon = NULL, html = NULL, @@ -52,6 +54,10 @@ layers (e.g. markers and polygons) can share the same group name.} \item{type}{The type of chart to use for clusters, either \code{c("pie","bar","horizontal","custom")}.} +\item{aggregation}{If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used.} + +\item{valueField}{If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column.} + \item{options}{Additional options for cluster charts (see \code{\link{clusterchartOptions}}).} \item{icon}{Include an icon or a set of icons with \code{makeIcon} or \code{iconList}} diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index 446720d5..5a87ac19 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -16,8 +16,6 @@ clusterchartOptions( labelStroke = "black", labelColor = "black", labelOpacity = 0.9, - aggregation = "sum", - valueField = NULL, digits = 2, sortTitlebyCount = TRUE ) @@ -45,10 +43,6 @@ clusterchartOptions( \item{labelOpacity}{The label color. Default is `0.9`} -\item{aggregation}{If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used.} - -\item{valueField}{If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column.} - \item{digits}{The amount of digits. Default is `2`} \item{sortTitlebyCount}{Should the svg-title be sorted by count or by the categories.} From 950bf9fdd2542e4b4cdd135c55f68a84bad45ce5 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Mon, 12 Aug 2024 23:36:16 +0200 Subject: [PATCH 19/26] docs, examples, details (CSS-styling), match.arg aggregation, fix example --- R/clusterCharts.R | 61 ++++++++++++++---- inst/examples/clustercharts_sum.R | 10 +-- .../lfx-clustercharts-bindings.js | 1 - man/addClusterCharts.Rd | 62 +++++++++++++++---- man/clusterchartOptions.Rd | 5 ++ tests/testthat/test-clustercharts.R | 4 +- 6 files changed, 111 insertions(+), 32 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 33b25936..3bfd57b6 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -17,21 +17,29 @@ clusterchartsDependencies <- function() { } #' addClusterCharts -#' @description Adds cluster charts (either pie or bar charts) to a Leaflet map. -#' @param type The type of chart to use for clusters, either \code{c("pie","bar","horizontal","custom")}. -#' @param categoryField The name of the feature property used to categorize the charts. -#' @param categoryMap A data frame mapping categories to chart properties (label, color, icons, stroke). -#' @param aggregation If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used. -#' @param valueField If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column. -#' @param icon Include an icon or a set of icons with \code{makeIcon} or \code{iconList} -#' @param html The html to include in the markers -#' @param popup Use the column name given in popup to collect the feature property with this name. -#' @param popupFields A string or vector of strings indicating the feature properties to include in popups. +#' @description Adds cluster charts (pie, bar, horizontal, or custom) to a Leaflet map. +#' @param type The type of chart to use for clusters: \code{"pie"}, \code{"bar"}, \code{"horizontal"}, or \code{"custom"}. +#' @param categoryField The column name used to categorize the charts. +#' @param categoryMap A data.frame mapping categories to chart properties (e.g., label, color, icons, stroke). +#' @param aggregation The aggregation method to use when \code{type = "custom"}. +#' @param valueField The column name containing values to be aggregated when \code{type = "custom"}. +#' @param icon An icon or set of icons to include, created with \code{makeIcon} or \code{iconList}. +#' @param html The column name containing the HTML content to include in the markers. +#' @param popup The column name used to retrieve feature properties for the popup. +#' @param popupFields A string or vector of strings indicating the column names to include in popups. #' @param popupLabels A string or vector of strings indicating the labels for the popup fields. #' @param options Additional options for cluster charts (see \code{\link{clusterchartOptions}}). #' @param legendOptions A list of options for the legend, including the title and position. #' @param markerOptions Additional options for markers (see \code{\link[leaflet:markerOptions]{markerOptions::markerOptions()}}). #' @inheritParams leaflet::addCircleMarkers +#' +#' @family clusterCharts +#' @details +#' The `clusterCharts` use Leaflet's `L.DivIcon`, allowing you to fully customize +#' the styling of individual markers and clusters using CSS. Each individual marker +#' within a cluster is assigned the CSS class `clustermarker`, while the entire +#' cluster is assigned the class `clustermarker-cluster`. You can modify the appearance +#' of these elements by targeting these classes in your custom CSS. #' @export #' @examples #' # Example usage: @@ -72,11 +80,40 @@ clusterchartsDependencies <- function() { #' , popupFields = c("brewery", "address", "zipcode", "category") #' , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") #' , label = "brewery" +#' +#' ## Custom Pie Chart with "mean" aggregation on column "value" +#' data <- sf::st_as_sf(breweries91) +#' categories <- c("Schwer", "Mäßig", "Leicht", "kein Schaden") +#' data$category <- sample(categories, size = nrow(data), replace = TRUE) +#' data$value <- round(runif(nrow(data), 0, 100), 0) +#' +#' leaflet() %>% +#' addProviderTiles("CartoDB.Positron") %>% +#' leaflet::addLayersControl(overlayGroups = "clustermarkers") %>% +#' addClusterCharts(data = data +#' , type = "custom" +#' , valueField = "value" +#' , aggregation = "mean" +#' , categoryField = "category" +#' , categoryMap = data.frame(labels = categories, +#' colors = c("#F88", "#FA0", "#FF3", "#BFB"), +#' strokes = "gray") +#' , options = clusterchartOptions(rmax=50, digits=0, innerRadius = 20) +#' , group = "clustermarkers" +#' , popupFields = c("brewery", "address", "zipcode", "category","value") +#' , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art", "Value") +#' , label = "brewery" +#' ) +#' +#' For Shiny examples, please run: +#' # runApp(system.file("examples/clusterCharts_app.R", package = "leaflet.extras2")) +#' # runApp(system.file("examples/clustercharts_sum.R", package = "leaflet.extras2")) #' ) addClusterCharts <- function( map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal","custom"), - aggregation = "sum", valueField = NULL, + aggregation = c("sum","min","max","mean","median"), + valueField = NULL, options = clusterchartOptions(), icon = NULL, html = NULL, popup = NULL, popupOptions = NULL, label = NULL, labelOptions = NULL, @@ -88,6 +125,7 @@ addClusterCharts <- function( ## Check arguments ############ type <- match.arg(type) + aggregation <- match.arg(aggregation) if (missing(labelOptions)) labelOptions <- labelOptions() if (missing(categoryMap)) { stop("The `categoryMap` is missing.\n", @@ -177,6 +215,7 @@ addClusterCharts <- function( #' @param digits The amount of digits. Default is `2` #' @param sortTitlebyCount Should the svg-title be sorted by count or by the categories. #' +#' @family clusterCharts #' @export clusterchartOptions <- function(rmax = 30, size = c(20, 20), width = 40, height = 50, diff --git a/inst/examples/clustercharts_sum.R b/inst/examples/clustercharts_sum.R index eb8a144e..d7b6f4e2 100644 --- a/inst/examples/clustercharts_sum.R +++ b/inst/examples/clustercharts_sum.R @@ -30,7 +30,6 @@ data$label <- paste0(data$brewery, "
", data$address) data$id <- paste0("ID", seq.int(nrow(data))) data$popup <- paste0("
", data$brewery, "
", data$address, "
") data$tosum <- sample(1:100, nrow(data), replace = TRUE) -data$tosum2 <- sample(1:10, nrow(data), replace = TRUE) data$tosumlabel <- paste("Sum: ", data$tosum) data$web <- gsub(">(.*?)<", ">",data$tosum,"<", data$web) data$web <- ifelse(is.na(data$web), "", paste0("
", data$web, "
")) @@ -48,9 +47,7 @@ ui <- fluidPage( selectInput("type", "Plot type", choices = c("bar","horizontal", "custom", "pie")), conditionalPanel("input.type == 'custom'", selectInput("aggr", "Aggregation", choices = c("sum","max", "min", "mean", - # "variance","deviation" ## working but not correct? ?? - # "cumsum", "mode", "least" ## not wokring - new d3 v? - "median"), selected = "mean") + "median"), selected = "sum") ), splitLayout(cellWidths = paste0(rep(20,4), "%"), div(h5("Click Event"), verbatimTextOutput("click")), @@ -70,7 +67,6 @@ server <- function(input, output, session) { addClusterCharts(data = data , options = clusterchartOptions(rmax = 50, size = 40, - # size = c(100,140), labelBackground = TRUE, labelOpacity = 0.5, innerRadius = 20, @@ -88,8 +84,8 @@ server <- function(input, output, session) { , group = "clustermarkers" , layerId = "id" , clusterId = "id" - # , popupFields = c("id","brewery","address","zipcode", "category","tosum","tosum2") - # , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum","tosum2") + , popupFields = c("id","brewery","address","zipcode", "category","tosum") + , popupLabels = c("id","Brauerei","Addresse","PLZ", "Art", "tosum") , label = "label" ## Options ############# , markerOptions = markerOptions(interactive = TRUE, diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 90f3261a..2a0ce526 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -55,7 +55,6 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, var myClass = 'clustermarker category-'+categoryVal+' icon-'+categoryVal; let extraInfo = { clusterId: clusterId }; - //console.log("feature"); console.log(feature) // Make DIV-Icon marker var myIcon = L.divIcon({ className: myClass, diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index 18c0732f..b118787c 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -11,7 +11,7 @@ addClusterCharts( layerId = NULL, group = NULL, type = c("pie", "bar", "horizontal", "custom"), - aggregation = "sum", + aggregation = c("sum", "min", "max", "mean", "median"), valueField = NULL, options = clusterchartOptions(), icon = NULL, @@ -52,19 +52,19 @@ Human-friendly group names are permitted--they need not be short, identifier-style names. Any number of layers and even different types of layers (e.g. markers and polygons) can share the same group name.} -\item{type}{The type of chart to use for clusters, either \code{c("pie","bar","horizontal","custom")}.} +\item{type}{The type of chart to use for clusters: \code{"pie"}, \code{"bar"}, \code{"horizontal"}, or \code{"custom"}.} -\item{aggregation}{If `type = "custom"` in the `addClusterCharts` function, this aggregation method will be used.} +\item{aggregation}{The aggregation method to use when \code{type = "custom"}.} -\item{valueField}{If `type = "custom"` in the `addClusterCharts` function, the aggregation will be used on this column.} +\item{valueField}{The column name containing values to be aggregated when \code{type = "custom"}.} \item{options}{Additional options for cluster charts (see \code{\link{clusterchartOptions}}).} -\item{icon}{Include an icon or a set of icons with \code{makeIcon} or \code{iconList}} +\item{icon}{An icon or set of icons to include, created with \code{makeIcon} or \code{iconList}.} -\item{html}{The html to include in the markers} +\item{html}{The column name containing the HTML content to include in the markers.} -\item{popup}{Use the column name given in popup to collect the feature property with this name.} +\item{popup}{The column name used to retrieve feature properties for the popup.} \item{popupOptions}{A Vector of \code{\link[leaflet]{popupOptions}} to provide popups} @@ -80,11 +80,11 @@ options} \item{clusterId}{the id for the marker cluster layer} -\item{categoryField}{The name of the feature property used to categorize the charts.} +\item{categoryField}{The column name used to categorize the charts.} -\item{categoryMap}{A data frame mapping categories to chart properties (label, color, icons, stroke).} +\item{categoryMap}{A data.frame mapping categories to chart properties (e.g., label, color, icons, stroke).} -\item{popupFields}{A string or vector of strings indicating the feature properties to include in popups.} +\item{popupFields}{A string or vector of strings indicating the column names to include in popups.} \item{popupLabels}{A string or vector of strings indicating the labels for the popup fields.} @@ -97,7 +97,14 @@ default, it is the \code{data} object provided to \code{leaflet()} initially, but can be overridden} } \description{ -Adds cluster charts (either pie or bar charts) to a Leaflet map. +Adds cluster charts (pie, bar, horizontal, or custom) to a Leaflet map. +} +\details{ +The `clusterCharts` use Leaflet's `L.DivIcon`, allowing you to fully customize +the styling of individual markers and clusters using CSS. Each individual marker +within a cluster is assigned the CSS class `clustermarker`, while the entire +cluster is assigned the class `clustermarker-cluster`. You can modify the appearance +of these elements by targeting these classes in your custom CSS. } \examples{ # Example usage: @@ -138,5 +145,38 @@ leaflet() \%>\% , popupFields = c("brewery", "address", "zipcode", "category") , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") , label = "brewery" + +## Custom Pie Chart with "mean" aggregation on column "value" +data <- sf::st_as_sf(breweries91) +categories <- c("Schwer", "Mäßig", "Leicht", "kein Schaden") +data$category <- sample(categories, size = nrow(data), replace = TRUE) +data$value <- round(runif(nrow(data), 0, 100), 0) + +leaflet() \%>\% + addProviderTiles("CartoDB.Positron") \%>\% + leaflet::addLayersControl(overlayGroups = "clustermarkers") \%>\% + addClusterCharts(data = data + , type = "custom" + , valueField = "value" + , aggregation = "mean" + , categoryField = "category" + , categoryMap = data.frame(labels = categories, + colors = c("#F88", "#FA0", "#FF3", "#BFB"), + strokes = "gray") + , options = clusterchartOptions(rmax=50, digits=0, innerRadius = 20) + , group = "clustermarkers" + , popupFields = c("brewery", "address", "zipcode", "category","value") + , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art", "Value") + , label = "brewery" + ) + +For Shiny examples, please run: +# runApp(system.file("examples/clusterCharts_app.R", package = "leaflet.extras2")) +# runApp(system.file("examples/clustercharts_sum.R", package = "leaflet.extras2")) ) } +\seealso{ +Other clusterCharts: +\code{\link{clusterchartOptions}()} +} +\concept{clusterCharts} diff --git a/man/clusterchartOptions.Rd b/man/clusterchartOptions.Rd index 5a87ac19..9031091f 100644 --- a/man/clusterchartOptions.Rd +++ b/man/clusterchartOptions.Rd @@ -50,3 +50,8 @@ clusterchartOptions( \description{ Adds options for clusterCharts } +\seealso{ +Other clusterCharts: +\code{\link{addClusterCharts}()} +} +\concept{clusterCharts} diff --git a/tests/testthat/test-clustercharts.R b/tests/testthat/test-clustercharts.R index cca1fe31..5a1624b8 100644 --- a/tests/testthat/test-clustercharts.R +++ b/tests/testthat/test-clustercharts.R @@ -225,6 +225,8 @@ test_that("clustercharts", { ## ALL ############ m <- leaflet() %>% addMapPane("clusterpane", 420) %>% addClusterCharts(data = data + , aggregation = "sum" + , valueField = "tosum" , options = clusterchartOptions(rmax = 50, size = 40, # size = c(100,140), @@ -233,8 +235,6 @@ test_that("clustercharts", { labelColor = "gray", labelOpacity = 0.5, innerRadius = 20, - aggregation = "sum", - valueField = "tosum", digits = 0, sortTitlebyCount = TRUE) # , type = "bar" From 876e5c0ce9b0dbb03b46f650ab46bf5d75251c59 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Mon, 12 Aug 2024 23:41:03 +0200 Subject: [PATCH 20/26] fix example --- R/clusterCharts.R | 5 ++--- man/addClusterCharts.Rd | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index 3bfd57b6..f5b455b5 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -79,7 +79,7 @@ clusterchartsDependencies <- function() { #' , group = "clustermarkers" #' , popupFields = c("brewery", "address", "zipcode", "category") #' , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") -#' , label = "brewery" +#' , label = "brewery") #' #' ## Custom Pie Chart with "mean" aggregation on column "value" #' data <- sf::st_as_sf(breweries91) @@ -105,10 +105,9 @@ clusterchartsDependencies <- function() { #' , label = "brewery" #' ) #' -#' For Shiny examples, please run: +#' ## For Shiny examples, please run: #' # runApp(system.file("examples/clusterCharts_app.R", package = "leaflet.extras2")) #' # runApp(system.file("examples/clustercharts_sum.R", package = "leaflet.extras2")) -#' ) addClusterCharts <- function( map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, type = c("pie","bar","horizontal","custom"), diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index b118787c..c1362304 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -144,7 +144,7 @@ leaflet() \%>\% , group = "clustermarkers" , popupFields = c("brewery", "address", "zipcode", "category") , popupLabels = c("Brauerei", "Adresse", "PLZ", "Art") - , label = "brewery" + , label = "brewery") ## Custom Pie Chart with "mean" aggregation on column "value" data <- sf::st_as_sf(breweries91) @@ -170,10 +170,9 @@ leaflet() \%>\% , label = "brewery" ) -For Shiny examples, please run: +## For Shiny examples, please run: # runApp(system.file("examples/clusterCharts_app.R", package = "leaflet.extras2")) # runApp(system.file("examples/clustercharts_sum.R", package = "leaflet.extras2")) - ) } \seealso{ Other clusterCharts: From 4408e124bf6634b0847187073b7d621a32c653df Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Tue, 13 Aug 2024 10:59:11 +0200 Subject: [PATCH 21/26] rename to bakeTheStatsChart --- .../lfx-clustercharts/lfx-clustercharts-bindings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js index 2a0ce526..01788e6f 100644 --- a/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js +++ b/inst/htmlwidgets/lfx-clustercharts/lfx-clustercharts-bindings.js @@ -160,7 +160,7 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, totalAggregation = totalAggregation.toFixed(digits); // Make Chart - html = bakeTheBubbleChart({ + html = bakeTheStatsChart({ data: data, valueFunc: function(d) { return d.values; }, outerRadius: r, @@ -239,8 +239,8 @@ LeafletWidget.methods.addClusterCharts = function(geojson, layerId, group, type, return myIcon; } - //function that generates a svg markup for a Bubble chart - function bakeTheBubbleChart(options) { + //function that generates a svg markup for a Statistics chart + function bakeTheStatsChart(options) { if (!options.data || !options.valueFunc) { return ''; } From b1ba2807c6f67df415a63857625fa7db0d1f7713 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sat, 31 Aug 2024 12:56:18 +0200 Subject: [PATCH 22/26] extend examples --- inst/examples/clusterCharts_app.R | 15 ++++++++------- inst/examples/clustercharts_sum.R | 16 +++++++++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/inst/examples/clusterCharts_app.R b/inst/examples/clusterCharts_app.R index 0826a27a..292a9335 100644 --- a/inst/examples/clusterCharts_app.R +++ b/inst/examples/clusterCharts_app.R @@ -33,6 +33,7 @@ data$web <- ifelse(is.na(data$web), "", paste0("
", data$ data$tosum <- sample(1:100, nrow(data), replace = TRUE) ui <- fluidPage( + titlePanel("Cluster Markers and Calculate Category Counts"), tags$head(tags$style(" .inputs { display: flex; @@ -61,10 +62,10 @@ ui <- fluidPage( )), leafletOutput("map", height = 650), splitLayout(cellWidths = paste0(rep(20,4), "%"), - div(verbatimTextOutput("click")), - div(verbatimTextOutput("mouseover")), - div(verbatimTextOutput("mouseout")), - div(verbatimTextOutput("dragend")) + div(h4("Click Event"), verbatimTextOutput("click")), + div(h4("Mouseover Event"), verbatimTextOutput("mouseover")), + div(h4("Mouseout Event"), verbatimTextOutput("mouseout")), + div(h4("Dragend Event"), verbatimTextOutput("dragend")) ) ) @@ -72,7 +73,7 @@ server <- function(input, output, session) { output$map <- renderLeaflet({ leaflet() %>% addMapPane("clusterpane", 420) %>% addProviderTiles("CartoDB") %>% - leaflet::addLayersControl(overlayGroups = c("clustermarkers","normalcircles")) %>% + leaflet::addLayersControl(overlayGroups = c("clustermarkers")) %>% # addCircleMarkers(data = data, group = "normalcircles", clusterOptions = markerClusterOptions()) %>% addClusterCharts(data = data , options = clusterchartOptions(rmax = input$rmax, @@ -101,7 +102,7 @@ server <- function(input, output, session) { # colors = c("cyan", "darkorange", "yellow", "#9fca8b"), # icons = c("icons/Icon29.svg", "icons/Icon8.svg", "icons/Icon5.svg", "icons/Icon25.svg"), # strokes = c("#800", "#B60", "#D80", "#070") - strokes = "gray" + strokes = "black" ) , group = "clustermarkers" , layerId = "id" @@ -116,7 +117,7 @@ server <- function(input, output, session) { keyboard = TRUE, title = "Some Marker Title", zIndexOffset = 100, - opacity = 0.6, + opacity = 1, riseOnHover = TRUE, riseOffset = 400) , legendOptions = list(position = "bottomright", title = "Unfälle im Jahr 2003") diff --git a/inst/examples/clustercharts_sum.R b/inst/examples/clustercharts_sum.R index d7b6f4e2..8154ca7c 100644 --- a/inst/examples/clustercharts_sum.R +++ b/inst/examples/clustercharts_sum.R @@ -36,6 +36,7 @@ data$web <- ifelse(is.na(data$web), "", paste0("
", data$ ## UI ############## ui <- fluidPage( + titlePanel("Cluster Markers and calculate Sum/Max/Min/Mean/Median across Categories"), tags$head(tags$style(" .markerhtml { height: 100%; @@ -44,16 +45,16 @@ ui <- fluidPage( position: absolute; }")), leafletOutput("map", height = 650), - selectInput("type", "Plot type", choices = c("bar","horizontal", "custom", "pie")), + selectInput("type", "Plot type", choices = c("bar","horizontal", "custom", "pie"), selected = "custom"), conditionalPanel("input.type == 'custom'", selectInput("aggr", "Aggregation", choices = c("sum","max", "min", "mean", "median"), selected = "sum") ), splitLayout(cellWidths = paste0(rep(20,4), "%"), - div(h5("Click Event"), verbatimTextOutput("click")), - div(h5("Mouseover Event"), verbatimTextOutput("mouseover")), - div(h5("Mouseout Event"), verbatimTextOutput("mouseout")), - div(h5("Drag-End Event"), verbatimTextOutput("dragend")) + div(h4("Click Event"), verbatimTextOutput("click")), + div(h4("Mouseover Event"), verbatimTextOutput("mouseover")), + div(h4("Mouseout Event"), verbatimTextOutput("mouseout")), + div(h4("Dragend Event"), verbatimTextOutput("dragend")) ) ) @@ -62,13 +63,14 @@ server <- function(input, output, session) { output$map <- renderLeaflet({ leaflet() %>% addMapPane("clusterpane", 420) %>% addProviderTiles("CartoDB") %>% - leaflet::addLayersControl(overlayGroups = c("clustermarkers","normalcircles")) %>% + leaflet::addLayersControl(overlayGroups = c("clustermarkers")) %>% # addCircleMarkers(data = data, group = "normalcircles", clusterOptions = markerClusterOptions()) %>% addClusterCharts(data = data , options = clusterchartOptions(rmax = 50, size = 40, labelBackground = TRUE, - labelOpacity = 0.5, + labelOpacity = 1, + strokeWidth = 0.1, innerRadius = 20, digits = 0, sortTitlebyCount = TRUE) From c30b30d3b6548d075652579ba552b033045764cc Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sat, 31 Aug 2024 13:14:36 +0200 Subject: [PATCH 23/26] docs --- R/clusterCharts.R | 11 +++++++---- man/addClusterCharts.Rd | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index f5b455b5..c9e58306 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -17,12 +17,15 @@ clusterchartsDependencies <- function() { } #' addClusterCharts -#' @description Adds cluster charts (pie, bar, horizontal, or custom) to a Leaflet map. +#' @description Clusters markers on a Leaflet map and visualizes them using +#' customizable charts, such as pie or bar charts, showing counts by category. +#' When using the \code{"custom"} type, a pie chart is rendered with aggregated data, +#' employing methods like sum, min, max, mean, or median. #' @param type The type of chart to use for clusters: \code{"pie"}, \code{"bar"}, \code{"horizontal"}, or \code{"custom"}. -#' @param categoryField The column name used to categorize the charts. +#' @param categoryField Column name for categorizing charts. #' @param categoryMap A data.frame mapping categories to chart properties (e.g., label, color, icons, stroke). -#' @param aggregation The aggregation method to use when \code{type = "custom"}. -#' @param valueField The column name containing values to be aggregated when \code{type = "custom"}. +#' @param aggregation Aggregation method for \code{"custom"} charts (e.g., sum, min, max, mean, median). +#' @param valueField Column name with values to aggregate for \code{"custom"} charts. #' @param icon An icon or set of icons to include, created with \code{makeIcon} or \code{iconList}. #' @param html The column name containing the HTML content to include in the markers. #' @param popup The column name used to retrieve feature properties for the popup. diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index c1362304..cd85c13a 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -54,9 +54,9 @@ layers (e.g. markers and polygons) can share the same group name.} \item{type}{The type of chart to use for clusters: \code{"pie"}, \code{"bar"}, \code{"horizontal"}, or \code{"custom"}.} -\item{aggregation}{The aggregation method to use when \code{type = "custom"}.} +\item{aggregation}{Aggregation method for \code{"custom"} charts (e.g., sum, min, max, mean, median).} -\item{valueField}{The column name containing values to be aggregated when \code{type = "custom"}.} +\item{valueField}{Column name with values to aggregate for \code{"custom"} charts.} \item{options}{Additional options for cluster charts (see \code{\link{clusterchartOptions}}).} @@ -80,7 +80,7 @@ options} \item{clusterId}{the id for the marker cluster layer} -\item{categoryField}{The column name used to categorize the charts.} +\item{categoryField}{Column name for categorizing charts.} \item{categoryMap}{A data.frame mapping categories to chart properties (e.g., label, color, icons, stroke).} @@ -97,7 +97,10 @@ default, it is the \code{data} object provided to \code{leaflet()} initially, but can be overridden} } \description{ -Adds cluster charts (pie, bar, horizontal, or custom) to a Leaflet map. +Clusters markers on a Leaflet map and visualizes them using +customizable charts, such as pie or bar charts, showing counts by category. +When using the \code{"custom"} type, a pie chart is rendered with aggregated data, +employing methods like sum, min, max, mean, or median. } \details{ The `clusterCharts` use Leaflet's `L.DivIcon`, allowing you to fully customize From c24bd81f7f8c1dc34f66c92ceecf1f9b12cb5f35 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sat, 31 Aug 2024 13:19:40 +0200 Subject: [PATCH 24/26] rm lat,lng --- R/clusterCharts.R | 4 ++-- man/addClusterCharts.Rd | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/R/clusterCharts.R b/R/clusterCharts.R index c9e58306..e96f1482 100644 --- a/R/clusterCharts.R +++ b/R/clusterCharts.R @@ -112,7 +112,7 @@ clusterchartsDependencies <- function() { #' # runApp(system.file("examples/clusterCharts_app.R", package = "leaflet.extras2")) #' # runApp(system.file("examples/clustercharts_sum.R", package = "leaflet.extras2")) addClusterCharts <- function( - map, lng = NULL, lat = NULL, layerId = NULL, group = NULL, + map, layerId = NULL, group = NULL, type = c("pie","bar","horizontal","custom"), aggregation = c("sum","min","max","mean","median"), valueField = NULL, @@ -188,7 +188,7 @@ addClusterCharts <- function( class(geojson) <- c("geojson","json") ## Derive Points and Invoke Method ################## - points <- derivePoints(data, lng, lat, missing(lng), missing(lat), + points <- derivePoints(data, NULL, NULL, TRUE, TRUE, "addClusterCharts") leaflet::invokeMethod( map, NULL, "addClusterCharts", geojson, layerId, group, type, diff --git a/man/addClusterCharts.Rd b/man/addClusterCharts.Rd index cd85c13a..4b15b2a6 100644 --- a/man/addClusterCharts.Rd +++ b/man/addClusterCharts.Rd @@ -6,8 +6,6 @@ \usage{ addClusterCharts( map, - lng = NULL, - lat = NULL, layerId = NULL, group = NULL, type = c("pie", "bar", "horizontal", "custom"), @@ -34,16 +32,6 @@ addClusterCharts( \arguments{ \item{map}{a map widget object created from \code{\link[leaflet]{leaflet}()}} -\item{lng}{a numeric vector of longitudes, or a one-sided formula of the form -\code{~x} where \code{x} is a variable in \code{data}; by default (if not -explicitly provided), it will be automatically inferred from \code{data} by -looking for a column named \code{lng}, \code{long}, or \code{longitude} -(case-insensitively)} - -\item{lat}{a vector of latitudes or a formula (similar to the \code{lng} -argument; the names \code{lat} and \code{latitude} are used when guessing -the latitude column from \code{data})} - \item{layerId}{the layer id} \item{group}{the name of the group the newly created layers should belong to From 72d0c144f34b5bd8268a0219c50884403bb84518 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sat, 31 Aug 2024 13:38:10 +0200 Subject: [PATCH 25/26] add Readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1cba6f58..b8f63ef7 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ If you need a plugin that is not already implemented create an [issue](https://g - [Tangram](https://github.com/tangrams/tangram) - [Velocity](https://github.com/onaci/leaflet-velocity) - [WMS](https://github.com/heigeo/leaflet.wms) +- [ClusterCharts](https://gist.github.com/gisminister/10001728) ### Documentation From 576969c9be0a957c8e3e3a8c587765f0a73c7366 Mon Sep 17 00:00:00 2001 From: Sebastian Gatscha Date: Sat, 31 Aug 2024 13:40:27 +0200 Subject: [PATCH 26/26] pkgdown clean --- _pkgdown.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/_pkgdown.yml b/_pkgdown.yml index 6a6310f7..2f3f9a73 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -27,7 +27,6 @@ reference: - markermenuItems - title: Clustercharts with d3 contents: - - matches("Contextmenu") - matches("ClusterCharts") - matches("clusterchart") - title: Easy Print