├── .gitignore ├── LICENSE ├── .Rbuildignore ├── NAMESPACE ├── .travis.yml ├── inst └── htmlwidgets │ ├── svgPanZoom.yaml │ ├── lib │ ├── hammer │ │ ├── README.md │ │ ├── LICENSE.md │ │ └── hammer.min.js │ └── svg-pan-zoom │ │ ├── LICENSE_svg-pan-zoom │ │ └── dist │ │ ├── svg-pan-zoom.min.js │ │ └── svg-pan-zoom.js │ └── svgPanZoom.js ├── cran-comments.md ├── svgPanZoom.Rproj ├── man ├── svgPanZoom-shiny.Rd └── svgPanZoom.Rd ├── NEWS.md ├── DESCRIPTION ├── R └── svgPanZoom.R └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2016 2 | COPYRIGHT HOLDER: Kent Russell 3 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | README.md 4 | cran-comments.md 5 | ^\.travis\.yml$ 6 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(renderSvgPanZoom) 4 | export(svgPanZoom) 5 | export(svgPanZoomOutput) 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | sudo: false 5 | cache: packages 6 | -------------------------------------------------------------------------------- /inst/htmlwidgets/svgPanZoom.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: hammer 3 | version: 2.0.8 4 | src: htmlwidgets/lib/hammer 5 | script: hammer.min.js 6 | - name: svg-pan-zoom 7 | version: 3.6.1 8 | src: htmlwidgets/lib/svg-pan-zoom 9 | script: dist/svg-pan-zoom.min.js 10 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | 3 | * local Windows install, R 3.6 4 | 5 | * ubuntu (on travis-ci), R 3.3.1 6 | 7 | * win-builder (devel and release) 8 | 9 | * rhub::check_for_cran() 10 | 11 | 12 | ## R CMD check results 13 | 14 | * checking package dependencies ... NOTE 15 | Packages which this enhances but not available for checking: 16 | 'gridSVG', 'knitr', 'XML', 'xml2' 17 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/hammer/README.md: -------------------------------------------------------------------------------- 1 | # Hammer.js 2.0 [![Build Status](https://travis-ci.org/hammerjs/hammer.js.svg)](https://travis-ci.org/hammerjs/hammer.js/) 2 | 3 | Visit [hammerjs.github.io](http://hammerjs.github.io) for documentation. 4 | 5 | You can get the pre-build versions from the Hammer.js website, or do this by yourself running 6 | `npm install -g grunt-cli && npm install && grunt build` 7 | -------------------------------------------------------------------------------- /svgPanZoom.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: XeLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /man/svgPanZoom-shiny.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/svgPanZoom.R 3 | \name{svgPanZoom-shiny} 4 | \alias{svgPanZoom-shiny} 5 | \alias{svgPanZoomOutput} 6 | \alias{renderSvgPanZoom} 7 | \title{Shiny bindings for svgPanZoom} 8 | \usage{ 9 | svgPanZoomOutput(outputId, width = "100\%", height = "400px") 10 | 11 | renderSvgPanZoom(expr, env = parent.frame(), quoted = FALSE) 12 | } 13 | \arguments{ 14 | \item{outputId}{output variable to read from} 15 | 16 | \item{width, height}{must be a valid CSS unit (like "100%", "400px", "auto") or a number, 17 | which will be coerced to a string and have "px" appended} 18 | 19 | \item{expr}{\code{expression} that generates a svgPanZoom htmlwidget} 20 | 21 | \item{env}{\code{environment} in which to evaluate \code{expr}} 22 | 23 | \item{quoted}{\code{logical} is \code{expr} a quoted \code{expression} (with \code{quote()})? 24 | This is useful if you want to save an \code{expression} in a variable.} 25 | } 26 | \description{ 27 | Shiny bindings for svgPanZoom 28 | } 29 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | svgPanZoom 0.3.4 2 | ------------------------------------------------------------------------ 3 | 4 | * update JavaScript libraries svg-pan-zoom to 3.6.1 and hammer to 2.0.8 5 | 6 | svgPanZoom 0.3.3 7 | ------------------------------------------------------------------------ 8 | 9 | * fix bug in zoom controls when using with `svglite` 10 | 11 | svgPanZoom 0.3.2 12 | ------------------------------------------------------------------------ 13 | 14 | * remove `SVGAnnotation` in examples in favor of `svglite` 15 | * Upgrade to svg-pan-zoom.js v3.2.6 16 | 17 | svgPanZoom 0.3.1 18 | ------------------------------------------------------------------------ 19 | 20 | * Add viewBox argument to disable "smart" viewBox 21 | 22 | svgPanZoom 0.3 23 | ------------------------------------------------------------------------ 24 | 25 | * Upgrade to svg-pan-zoom.js v3.2.3 26 | * Adds touch support with hammer.js 27 | * Internally add better sizing to fit the htmlwidget container 28 | * Add the ability to incorporate tasks on the JavaScript side 29 | * Work well with xml2 30 | 31 | svgPanZoom 0.2 32 | ------------------------------------------------------------------------ 33 | 34 | * Initial release to CRAN 35 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/hammer/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2011-2014 by Jorik Tangelder (Eight Media) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/svg-pan-zoom/LICENSE_svg-pan-zoom: -------------------------------------------------------------------------------- 1 | Copyright 2009-2010 Andrea Leofreddi 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: svgPanZoom 2 | Title: R 'Htmlwidget' to Add Pan and Zoom to Almost any R Graphic 3 | Version: 0.3.4 4 | Date: 2020-02-15 5 | Authors@R: c( 6 | person( 7 | "Anders", "Riutta et. al.", role = c("aut", "cph"), 8 | comment = "svg-pan-zoom.js BSD-licensed library in htmlwidgets/lib, 9 | https://github.com/ariutta/svg-pan-zoom" 10 | ), 11 | person( 12 | "Jorik", "Tangelder", role = c("aut", "cph"), 13 | comment = "hammer.js MIT-licensed touch library in htmlwidgets/lib, 14 | https://github.com/hammerjs/hammer" 15 | ), 16 | person( 17 | "Kent", "Russell", role = c("aut", "cre"), 18 | comment = "R interface to svg-pan-zoom.js", 19 | email = "kent.russell@timelyportfolio.com" 20 | ) 21 | ) 22 | Maintainer: Kent Russell 23 | Description: This 'htmlwidget' provides pan and zoom interactivity to R 24 | graphics, including 'base', 'lattice', and 'ggplot2'. The interactivity is 25 | provided through the 'svg-pan-zoom.js' library. Various options to the widget 26 | can tailor the pan and zoom experience to nearly any user desire. 27 | URL: https://github.com/timelyportfolio/svgPanZoom 28 | BugReports: https://github.com/timelyportfolio/svgPanZoom/issues 29 | License: MIT + file LICENSE 30 | Depends: 31 | R (>= 3.1.2) 32 | Imports: 33 | htmlwidgets (>= 0.3.2) 34 | Suggests: 35 | htmltools, 36 | svglite 37 | Enhances: 38 | gridSVG, 39 | knitr, 40 | XML, 41 | xml2 42 | RoxygenNote: 7.0.2 43 | -------------------------------------------------------------------------------- /man/svgPanZoom.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/svgPanZoom.R 3 | \name{svgPanZoom} 4 | \alias{svgPanZoom} 5 | \title{Pan and Zoom R graphics} 6 | \usage{ 7 | svgPanZoom( 8 | svg, 9 | viewBox = TRUE, 10 | ..., 11 | width = NULL, 12 | height = NULL, 13 | elementId = NULL 14 | ) 15 | } 16 | \arguments{ 17 | \item{svg}{one of 18 | \itemize{ 19 | \item svg - SVG as XML or xml2, such as return from \code{\link[svglite]{xmlSVG}} 20 | \item lattice plot - trellis object, such as \code{l} in \code{l=xyplot(...)} 21 | \item ggplot2 plot - ggplot object, such as \code{g} in \code{g=ggplot(...) + geom_line()} 22 | \item filename or connection of a SVG file 23 | }} 24 | 25 | \item{viewBox}{\code{logical} to add back the viewBox to the SVG. 26 | The default is \code{TRUE} to fit the svgPanZoom in its 27 | container.} 28 | 29 | \item{...}{other configuration options for svg-pan-zoom.js. 30 | See \href{https://github.com/ariutta/svg-pan-zoom#how-to-use}{svg-pan-zoom How To Use} 31 | for a full description of the options available. As an example to turn on 32 | \code{controlIconsEnabled} and turn , 33 | do \code{svgPanZoom( ..., controlIconsEnabled = TRUE, panEnabled = FALSE )}.} 34 | 35 | \item{width, height}{valid CSS unit (like "100\%", "400px", "auto") or a number, 36 | which will be coerced to a string and have "px" appended} 37 | 38 | \item{elementId}{\code{string} id for the \code{svgPanZoom} container. Since \code{svgPanZoom} 39 | does not display its container, this is very unlikely to be anything other than the 40 | default \code{NULL}.} 41 | } 42 | \description{ 43 | Add panning and zooming to almost any R graphics from base graphics, 44 | lattice, and ggplot2 by using the JavaScript library 45 | \href{https://github.com/ariutta/svg-pan-zoom}{svg-pan-zoom}. 46 | } 47 | \examples{ 48 | # svgPanZoom tries to be very flexible with its first argument 49 | 50 | # in this first example use SVG as a character string 51 | # this is probably the least likely use case 52 | library(svgPanZoom) 53 | svgPanZoom(' 54 | 55 | 56 | 57 | ') 58 | 59 | 60 | \dontrun{ 61 | library(svgPanZoom) 62 | 63 | # first let's demonstrate a base plot 64 | # use svglite for now 65 | library(svglite) 66 | library(lattice) 67 | svgPanZoom( svglite:::inlineSVG( plot(1:10) ) ) 68 | 69 | svgPanZoom(svglite:::inlineSVG(show( xyplot( y~x, data.frame(x=1:10,y=1:10) ) ))) 70 | 71 | # the package gridSVG is highly recommended for lattice and ggplot2 72 | # second let's demonstrate a lattice plot 73 | library(lattice) 74 | svgPanZoom( xyplot( y~x, data.frame(x=1:10,y=1:10) ) ) 75 | 76 | # third with a ggplot2 plot 77 | library(ggplot2) 78 | svgPanZoom( ggplot( data.frame(x=1:10,y=1:10), aes(x=x,y=y) ) + geom_line() ) 79 | 80 | #Of course as a good htmlwidget should, it works with Shiny also. 81 | library(shiny) 82 | library(svglite) 83 | library(svgPanZoom) 84 | library(ggplot2) 85 | 86 | ui <- shinyUI(bootstrapPage( 87 | svgPanZoomOutput(outputId = "main_plot") 88 | )) 89 | 90 | server = shinyServer(function(input, output) { 91 | output$main_plot <- renderSvgPanZoom({ 92 | p <- ggplot() + 93 | geom_point( 94 | data=data.frame(faithful),aes(x=eruptions,y=waiting) 95 | ) + 96 | stat_density2d( 97 | data=data.frame(faithful) 98 | ,aes(x=eruptions,y=waiting ,alpha =..level..) 99 | ,geom="polygon") + 100 | scale_alpha_continuous(range=c(0.05,0.2)) 101 | 102 | svgPanZoom(p, controlIconsEnabled = T) 103 | }) 104 | }) 105 | 106 | runApp(list(ui=ui,server=server)) 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /inst/htmlwidgets/svgPanZoom.js: -------------------------------------------------------------------------------- 1 | HTMLWidgets.widget({ 2 | 3 | name: 'svgPanZoom', 4 | 5 | type: 'output', 6 | 7 | initialize: function(el, width, height) { 8 | 9 | return { 10 | // TODO: add instance fields as required 11 | } 12 | 13 | }, 14 | 15 | renderValue: function(el, x, instance) { 16 | // set innerHTML equal to the SVG provided by R as x.svg 17 | // for better robustness, create DOM element separately 18 | // and add to the htmlwidgets container el 19 | el.innerHTML = x.svg; 20 | 21 | var svg = el.getElementsByTagName("svg")[0]; 22 | var viewbox = svg.getAttribute("viewBox"); 23 | 24 | // add touch with hammer.js 25 | // using code from example 26 | // https://github.com/ariutta/svg-pan-zoom/blob/master/demo/mobile.html 27 | x.config.customEventsHandler = { 28 | haltEventListeners: ['touchstart', 'touchend', 'touchmove', 'touchleave', 'touchcancel'] 29 | , init: function(options) { 30 | var instance = options.instance 31 | , initialScale = 1 32 | , pannedX = 0 33 | , pannedY = 0 34 | // Init Hammer 35 | // Listen only for pointer and touch events 36 | this.hammer = new Hammer(options.svgElement, { 37 | inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput 38 | }) 39 | // Enable pinch 40 | this.hammer.get('pinch').set({enable: true}) 41 | // Handle double tap 42 | this.hammer.on('doubletap', function(ev){ 43 | instance.zoomIn() 44 | }) 45 | // Handle pan 46 | this.hammer.on('panstart panmove', function(ev){ 47 | // On pan start reset panned variables 48 | if (ev.type === 'panstart') { 49 | pannedX = 0 50 | pannedY = 0 51 | } 52 | // Pan only the difference 53 | instance.panBy({x: ev.deltaX - pannedX, y: ev.deltaY - pannedY}) 54 | pannedX = ev.deltaX 55 | pannedY = ev.deltaY 56 | }) 57 | // Handle pinch 58 | this.hammer.on('pinchstart pinchmove', function(ev){ 59 | // On pinch start remember initial zoom 60 | if (ev.type === 'pinchstart') { 61 | initialScale = instance.getZoom() 62 | instance.zoom(initialScale * ev.scale) 63 | } 64 | instance.zoom(initialScale * ev.scale) 65 | }) 66 | // Prevent moving the page on some devices when panning over SVG 67 | options.svgElement.addEventListener('touchmove', function(e){ e.preventDefault(); }); 68 | } 69 | , destroy: function(){ 70 | this.hammer.destroy(); 71 | } 72 | } 73 | 74 | instance.zoomWidget = svgPanZoom(svg, x.config); 75 | 76 | // add back viewBox that svgPanZoom removes to fill the container 77 | // make it an argument on the R side in case 78 | // we want to disable 79 | if(x.options.viewBox){ 80 | // if viewbox previously defined take max of prior and bounding rect 81 | if(viewbox){ 82 | viewbox_array = viewbox.split(/[\s,\,]/) 83 | viewbox = [ 84 | viewbox_array[0], 85 | viewbox_array[1], 86 | Math.max(viewbox_array[2],svg.getBoundingClientRect().width), 87 | Math.max(viewbox_array[3],svg.getBoundingClientRect().height) 88 | ].join(" ") 89 | svg.setAttribute('viewBox', viewbox); 90 | } else { 91 | svg.setAttribute( 92 | 'viewBox', 93 | ['0','0', 94 | svg.getBoundingClientRect().width, 95 | svg.getBoundingClientRect().height 96 | ].join(' ') 97 | ) 98 | } 99 | } 100 | 101 | // use this to sort of make our diagram responsive 102 | // or at a minimum fit within the bounds set by htmlwidgets 103 | // for the parent container 104 | function makeResponsive(el){ 105 | var svg = el.getElementsByTagName("svg")[0]; 106 | if(svg){ 107 | if(svg.width) {svg.removeAttribute("width")}; 108 | if(svg.height) {svg.removeAttribute("height")}; 109 | svg.style.width = "100%"; 110 | svg.style.height = "100%"; 111 | } 112 | }; 113 | 114 | makeResponsive(el); 115 | 116 | if(!x.options.viewBox){ 117 | instance.zoomWidget.destroy(); 118 | instance.zoomWidget = svgPanZoom(svg, x.config); 119 | } 120 | 121 | // svglite css takes precedence 122 | // so style svgPanZoom more specifically 123 | // so the controls show up 124 | function styleZoomControls(el){ 125 | Array.prototype.map.call( 126 | document.querySelectorAll("#" + el.id + " .svg-pan-zoom-control > path"), 127 | function(d,i){d.style.fill="black"} 128 | ); 129 | }; 130 | 131 | styleZoomControls(el); 132 | 133 | // set up a container for tasks to perform after completion 134 | // one example would be add callbacks for event handling 135 | // styling 136 | if (!(typeof x.tasks === "undefined") ){ 137 | if ( (typeof x.tasks.length === "undefined") || 138 | (typeof x.tasks === "function" ) ) { 139 | // handle a function not enclosed in array 140 | // should be able to remove once using jsonlite 141 | x.tasks = [x.tasks]; 142 | } 143 | x.tasks.map(function(t){ 144 | // for each tasks call the task with el supplied as `this` 145 | t.call({el:el,zoomWidget:instance.zoomWidget}); 146 | }); 147 | } 148 | 149 | // use expando property so we can access later 150 | // somewhere saw where expando can cause memory leak in IE 151 | // could also set in HTMLWidgets.widgets[x] where matches el 152 | el.zoomWidget = instance.zoomWidget; 153 | 154 | }, 155 | 156 | resize: function(el, width, height, instance) { 157 | 158 | } 159 | 160 | }); 161 | -------------------------------------------------------------------------------- /R/svgPanZoom.R: -------------------------------------------------------------------------------- 1 | #' Pan and Zoom R graphics 2 | #' 3 | #' Add panning and zooming to almost any R graphics from base graphics, 4 | #' lattice, and ggplot2 by using the JavaScript library 5 | #' \href{https://github.com/ariutta/svg-pan-zoom}{svg-pan-zoom}. 6 | #' 7 | #' @param svg one of 8 | #' \itemize{ 9 | #' \item svg - SVG as XML or xml2, such as return from \code{\link[svglite]{xmlSVG}} 10 | #' \item lattice plot - trellis object, such as \code{l} in \code{l=xyplot(...)} 11 | #' \item ggplot2 plot - ggplot object, such as \code{g} in \code{g=ggplot(...) + geom_line()} 12 | #' \item filename or connection of a SVG file 13 | #' } 14 | #' @param viewBox \code{logical} to add back the viewBox to the SVG. 15 | #' The default is \code{TRUE} to fit the svgPanZoom in its 16 | #' container. 17 | #' @param ... other configuration options for svg-pan-zoom.js. 18 | #' See \href{https://github.com/ariutta/svg-pan-zoom#how-to-use}{svg-pan-zoom How To Use} 19 | #' for a full description of the options available. As an example to turn on 20 | #' \code{controlIconsEnabled} and turn , 21 | #' do \code{svgPanZoom( ..., controlIconsEnabled = TRUE, panEnabled = FALSE )}. 22 | #' @param width,height valid CSS unit (like "100\%", "400px", "auto") or a number, 23 | #' which will be coerced to a string and have "px" appended 24 | #' @param elementId \code{string} id for the \code{svgPanZoom} container. Since \code{svgPanZoom} 25 | #' does not display its container, this is very unlikely to be anything other than the 26 | #' default \code{NULL}. 27 | #' 28 | #' @examples 29 | #' # svgPanZoom tries to be very flexible with its first argument 30 | #' 31 | #' # in this first example use SVG as a character string 32 | #' # this is probably the least likely use case 33 | #' library(svgPanZoom) 34 | #' svgPanZoom(' 35 | #' 36 | #' 37 | #' 38 | #' ') 39 | #' 40 | #' 41 | #' \dontrun{ 42 | #' library(svgPanZoom) 43 | #' 44 | #' # first let's demonstrate a base plot 45 | #' # use svglite for now 46 | #' library(svglite) 47 | #' library(lattice) 48 | #' svgPanZoom( svglite:::inlineSVG( plot(1:10) ) ) 49 | #' 50 | #' svgPanZoom(svglite:::inlineSVG(show( xyplot( y~x, data.frame(x=1:10,y=1:10) ) ))) 51 | #' 52 | #' # the package gridSVG is highly recommended for lattice and ggplot2 53 | #' # second let's demonstrate a lattice plot 54 | #' library(lattice) 55 | #' svgPanZoom( xyplot( y~x, data.frame(x=1:10,y=1:10) ) ) 56 | #' 57 | #' # third with a ggplot2 plot 58 | #' library(ggplot2) 59 | #' svgPanZoom( ggplot( data.frame(x=1:10,y=1:10), aes(x=x,y=y) ) + geom_line() ) 60 | #' 61 | #' #Of course as a good htmlwidget should, it works with Shiny also. 62 | #' library(shiny) 63 | #' library(svglite) 64 | #' library(svgPanZoom) 65 | #' library(ggplot2) 66 | #' 67 | #' ui <- shinyUI(bootstrapPage( 68 | #' svgPanZoomOutput(outputId = "main_plot") 69 | #' )) 70 | #' 71 | #' server = shinyServer(function(input, output) { 72 | #' output$main_plot <- renderSvgPanZoom({ 73 | #' p <- ggplot() + 74 | #' geom_point( 75 | #' data=data.frame(faithful),aes(x=eruptions,y=waiting) 76 | #' ) + 77 | #' stat_density2d( 78 | #' data=data.frame(faithful) 79 | #' ,aes(x=eruptions,y=waiting ,alpha =..level..) 80 | #' ,geom="polygon") + 81 | #' scale_alpha_continuous(range=c(0.05,0.2)) 82 | #' 83 | #' svgPanZoom(p, controlIconsEnabled = T) 84 | #' }) 85 | #' }) 86 | #' 87 | #' runApp(list(ui=ui,server=server)) 88 | #' } 89 | #' 90 | #' @export 91 | svgPanZoom <- function(svg, viewBox = TRUE, ... , width = NULL, height = NULL, elementId = NULL) { 92 | 93 | # check to see if trellis for lattice or ggplot 94 | if(inherits(svg,c("trellis","ggplot","ggmultiplot"))){ 95 | # if class is trellis then plot then use grid.export 96 | # try to use gridSVG if available 97 | if (requireNamespace("gridSVG", quietly = TRUE)) { 98 | print(svg) 99 | svg = gridSVG::grid.export(name=NULL)$svg 100 | } else { #use svglite 101 | if(requireNamespace("svglite", quietly = TRUE)){ 102 | warning("for best results with ggplot2 and lattice, please install gridSVG") 103 | svg = svglite::xmlSVG(svg) 104 | } else { # if 105 | stop( 106 | "svglite or gridSVG required with lattice or trellis objects", 107 | call. = FALSE 108 | ) 109 | } 110 | } 111 | } 112 | 113 | # check to see if svg is XML and saveXML if so 114 | if(inherits(svg,c("XMLAbstractDocument","XMLAbstractNode"))){ 115 | # should we add check for svg element? 116 | svg = XML::saveXML(svg) 117 | } 118 | 119 | # check for "xml_document" from xml2 120 | if(inherits(svg,c("xml_document","xml_node"))){ 121 | svg = as.character(svg) 122 | } 123 | 124 | # use SVG file if provided 125 | # thanks @jjallaire for code from https://github.com/rich-iannone/DiagrammeR 126 | # to use file or connection 127 | if ( inherits(svg, "connection") || ( class(svg) == "character" && file.exists(svg) ) ){ 128 | # might want to parse to insure validity 129 | svg <- readLines(svg, warn = FALSE) 130 | } 131 | 132 | 133 | # forward options using x 134 | x = list( 135 | svg = svg 136 | ,config = list(...) 137 | ,options = list( 138 | viewBox = viewBox 139 | ) 140 | ) 141 | 142 | # create widget 143 | htmlwidgets::createWidget( 144 | name = 'svgPanZoom', 145 | x, 146 | width = width, 147 | height = height, 148 | package = 'svgPanZoom', 149 | elementId = elementId 150 | ) 151 | } 152 | 153 | #' Shiny bindings for svgPanZoom 154 | #' 155 | #' @param outputId output variable to read from 156 | #' @param width,height must be a valid CSS unit (like "100%", "400px", "auto") or a number, 157 | #' which will be coerced to a string and have "px" appended 158 | #' @param expr \code{expression} that generates a svgPanZoom htmlwidget 159 | #' @param env \code{environment} in which to evaluate \code{expr} 160 | #' @param quoted \code{logical} is \code{expr} a quoted \code{expression} (with \code{quote()})? 161 | #' This is useful if you want to save an \code{expression} in a variable. 162 | #' 163 | #' @name svgPanZoom-shiny 164 | #' 165 | #' @export 166 | svgPanZoomOutput <- function(outputId, width = '100%', height = '400px'){ 167 | htmlwidgets::shinyWidgetOutput(outputId, 'svgPanZoom', width, height, package = 'svgPanZoom') 168 | } 169 | 170 | #' @rdname svgPanZoom-shiny 171 | #' 172 | #' @export 173 | renderSvgPanZoom <- function(expr, env = parent.frame(), quoted = FALSE) { 174 | if (!quoted) { expr <- substitute(expr) } # force quoted 175 | htmlwidgets::shinyRenderWidget(expr, svgPanZoomOutput, env, quoted = TRUE) 176 | } 177 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ### svgPanZoom - [htmlwidget](http://htmlwidgets.org) to Pan / Zoom R graphics 2 | 3 | [![Build Status](https://travis-ci.org/timelyportfolio/svgPanZoom.png?branch=master)](https://travis-ci.org/timelyportfolio/svgPanZoom) 4 | [![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/svgPanZoom)](https://cran.r-project.org/package=svgPanZoom) 5 | 6 | `svgPanZoom` is a [`htmlwidgets`](http://htmlwidgets.org) wrapper for [`svg-pan-zoom.js`](https://github.com/ariutta/svg-pan-zoom). `svgPanZoom` gives `R` users an easy way to add panning and zooming to any `R` graphics (base, ggplot2, lattice, and lots more). It prioritizes ease and simplicity, but does offer some ability to tailor the experience and enhance the interactivity. 7 | 8 | ### Install It 9 | To get started, you may choose from CRAN `install.packages("devtools")` or Github `remotes::install_github("timelyportfolio/svgPanZoom")`. `svgPanZoom` is fairly stable, so CRAN is likely your best choice. 10 | 11 | 12 | ### Use It 13 | As stated in the introduction `svgPanZoom` works with almost all `R` graphics types. For `base` graphics, we'll need the `svglite` package. 14 | 15 | ``` 16 | library(svgPanZoom) # see install step above 17 | library(svglite) 18 | 19 | svgPanZoom( 20 | svglite:::inlineSVG( 21 | plot(1:10) 22 | ) 23 | ) 24 | ``` 25 | 26 | ### Use It in Shiny 27 | 28 | There are lots more examples below, but real quickly here is how we can use it in Shiny. 29 | 30 | ```R 31 | library(shiny) 32 | library(svglite) 33 | library(svgPanZoom) 34 | library(ggplot2) 35 | 36 | ui <- shinyUI(bootstrapPage( 37 | 38 | svgPanZoomOutput(outputId = "main_plot") 39 | 40 | )) 41 | 42 | server = shinyServer(function(input, output) { 43 | output$main_plot <- renderSvgPanZoom({ 44 | p <- ggplot() + geom_point(data=data.frame(faithful),aes(x=eruptions,y=waiting)) + stat_density2d(data=data.frame(faithful),aes(x=eruptions,y=waiting, alpha =..level..),geom="polygon") + scale_alpha_continuous(range=c(0.05,0.2)) 45 | svgPanZoom(p, controlIconsEnabled = T) 46 | }) 47 | }) 48 | 49 | runApp(list(ui=ui,server=server)) 50 | ``` 51 | 52 | ### Use It With Grid and More 53 | 54 | `svglite` also works with `grid` graphics, such as `ggplot2` and `lattice`. Before I show an example though, I **highly recommend** using `gridSVG` for `ggplot2` and `lattice`. For some good reasons, please see [this](http://stattech.wordpress.fos.auckland.ac.nz/2013-4-generating-structured-and-labelled-svg/) from Paul Murrell and Simon Potter. If you are making big graphics--think maps, multiple graphs, etc.--for **speed stick with `svglite`**. Here is a simple example using `ggplot2` with `svglite` and `svglite:::inlineSVG`. 55 | 56 | ``` 57 | library(svgPanZoom) 58 | library(svglite) 59 | library(ggplot2) 60 | 61 | svgPanZoom( 62 | svglite:::inlineSVG( 63 | #will put on separate line but also need show 64 | show( 65 | ggplot(data.frame(x=1:10,y=1:10),aes(x=x,y=y)) + geom_line() 66 | 67 | ) 68 | ) 69 | ) 70 | ``` 71 | 72 | Now let's do that same plot with `gridSVG`. 73 | 74 | ``` 75 | svgPanZoom( 76 | ggplot(data.frame(x=1:10,y=1:10),aes(x=x,y=y)) + geom_line() 77 | ) 78 | ``` 79 | 80 | You might notice right off that sizing is better handled, but more importantly, the resulting `SVG` is much better structured `XML`. That small time lag will really start to hurt if you are using `gridSVG` with large or complicated graphics. So if speed is important, ignore the better structure from `gridSVG` and stick with `svglite`. 81 | 82 | As promised, `lattice` (yes, I still use it and like it) works just as nicely. 83 | 84 | ``` 85 | library(svgPanZoom) 86 | library(svglite) 87 | library(lattice) 88 | 89 | # with gridSVG 90 | svgPanZoom( 91 | xyplot( y~x, data.frame(x=1:10,y=1:10), type = "b" ) 92 | ) 93 | 94 | # with svgPlot 95 | svgPanZoom( 96 | svglite:::inlineSVG( 97 | show(xyplot( y~x, data.frame(x=1:10,y=1:10), type = "b" )) 98 | ) 99 | ) 100 | ``` 101 | 102 | If you are not impressed yet, then maybe the graphics were not compelling enough. Let's add `svgPanZoom` to some more complicated visualizations. 103 | 104 | ``` 105 | library(choroplethr) 106 | # from chorplethr documetation 107 | data(df_pop_state) 108 | m = state_choropleth( 109 | df_pop_state 110 | , title="US 2012 State Population Estimates" 111 | , legend="Population" 112 | ) 113 | # take a peek 114 | m 115 | # would be so much more fun with pan and zoom 116 | svgPanZoom( m ) 117 | # if your map is big and hairy do this instead 118 | svgPanZoom( 119 | svglite:::inlineSVG( 120 | show(m ) 121 | # will have to manually size the svg device 122 | , height = 10, width = 16 ) 123 | ) 124 | 125 | ``` 126 | 127 | How about using [`ggfortify`](https://github.com/sinhrks/ggfortify)? Here are some great [examples](http://rpubs.com/sinhrks). Let's make some pan and zoom. 128 | 129 | ``` 130 | # devtools::install_github('sinhrks/ggfortify') 131 | library(ggfortify) 132 | svgPanZoom( 133 | autoplot(lm(Petal.Width ~ Petal.Length, data = iris)) 134 | ) 135 | 136 | library(survival) 137 | svgPanZoom( 138 | autoplot(survfit(Surv(time, status) ~ sex, data = lung)) 139 | ) 140 | ``` 141 | 142 | If ternary diagrams excite you, let's do this [USDA soil example](http://www.ggtern.com/2014/01/15/usda-textural-soil-classification/) from [`ggtern`](http://www.ggtern.com). 143 | 144 | ``` 145 | #install.packages("ggtern") 146 | 147 | # use example from ?ggtern::USDA 148 | library(ggtern) 149 | library(plyr) 150 | #Load the Data. 151 | data(USDA) 152 | #Put tile labels at the midpoint of each tile. 153 | USDA.LAB <- ddply(USDA,"Label",function(df){apply(df[,1:3],2,mean)}) 154 | #Tweak 155 | USDA.LAB$Angle=0; USDA.LAB$Angle[which(USDA.LAB$Label == "Loamy Sand")] = -35 156 | 157 | # Construct the plot. 158 | gTern <- ggtern(data=USDA,aes(Sand,Clay,Silt,color=Label,fill=Label)) + 159 | geom_polygon(alpha=0.75,size=0.5,color="black") + 160 | geom_mask() + 161 | geom_text(data=USDA.LAB,aes(label=Label,angle=Angle),color="black",size=3.5) + 162 | theme_rgbw() + 163 | theme_showsecondary() + 164 | theme_showarrows() + 165 | weight_percent() + guides(fill='none') + 166 | theme_legend_position("topleft") 167 | labs( 168 | title="USDA Textural Classification Chart", 169 | fill="Textural Class",color="Textural Class" 170 | ) 171 | 172 | 173 | svgPanZoom(gTern) 174 | ``` 175 | 176 | 177 | The true test for me though will be financial time series plots. Will `svgPanZoom` pass the test? 178 | 179 | ``` 180 | library(svgPanZoom) 181 | library(svglite) 182 | library(PerformanceAnalytics) 183 | 184 | data(edhec) 185 | 186 | svgPanZoom( 187 | svglite:::inlineSVG( 188 | charts.PerformanceSummary( 189 | edhec 190 | ,main = "Performance of EDHEC Indicies" 191 | ) 192 | ), 193 | controlIconsEnabled = TRUE 194 | ) 195 | 196 | library(quantmod) 197 | getSymbols("JPM", from = "2013-12-31") 198 | svgPanZoom( 199 | svglite:::inlineSVG( 200 | chartSeries(JPM, theme = chartTheme('white'), 201 | multi.col=T,TA="addVo();addBBands();addCCI()") 202 | ,height = 7 203 | ,width = 12 204 | ) 205 | ) 206 | ``` 207 | 208 | 209 | Tal Galili's [`dendextend`](https://github.com/talgalili/dendextend) offers another great use case for pan and zoom interaction. Let's look at one of the examples from the package vignette. 210 | 211 | ``` 212 | # install.packages("dendextend") 213 | library(dendextend) 214 | library(svglite) 215 | library(svgPanZoom) 216 | 217 | data(iris) 218 | d_iris <- dist(iris[,-5]) # method="man" # is a bit better 219 | hc_iris <- hclust(d_iris) 220 | dend_iris <- as.dendrogram(hc_iris) 221 | iris_species <- rev(levels(iris[,5])) 222 | dend_iris <- color_branches(dend_iris,k=3, groupLabels=iris_species) 223 | # have the labels match the real classification of the flowers: 224 | labels_colors(dend_iris) <- 225 | rainbow_hcl(3)[sort_levels_values( 226 | as.numeric(iris[,5])[order.dendrogram(dend_iris)] 227 | )] 228 | 229 | # We'll add the flower type 230 | labels(dend_iris) <- paste(as.character(iris[,5])[order.dendrogram(dend_iris)], 231 | "(",labels(dend_iris),")", 232 | sep = "") 233 | 234 | dend_iris <- hang.dendrogram(dend_iris,hang_height=0.1) 235 | 236 | # reduce the size of the labels: 237 | dend_iris <- assign_values_to_leaves_nodePar(dend_iris, 0.5, "lab.cex") 238 | 239 | par(mar = c(3,3,3,7)) 240 | svglite:::inlineSVG( 241 | { 242 | plot(dend_iris, 243 | main = "Clustered Iris dataset 244 | (the labels give the true flower species)", 245 | horiz = TRUE, nodePar = list(cex = .007)) 246 | legend("topleft", legend = iris_species, fill = rainbow_hcl(3)) 247 | } 248 | , height = 12, width = 14 249 | ) %>% svgPanZoom 250 | ``` 251 | 252 | For what I consider the ultimate test, will it work with `HiveR`? 253 | 254 | ``` 255 | # from HiveR documentation ?plotHive 256 | library(HiveR) 257 | library(grid) 258 | library(svglite) 259 | library(svgPanZoom) 260 | 261 | data(HEC) 262 | svgPanZoom( 263 | svglite:::inlineSVG( 264 | { 265 | data(HEC) 266 | currDir = getwd() 267 | setwd(system.file("extdata", "Misc", package = "HiveR")) 268 | plotHive(HEC, ch = 0.1, bkgnd = "white", 269 | axLabs = c("hair\ncolor", "eye\ncolor"), 270 | axLab.pos = c(1, 1), 271 | axLab.gpar = gpar(fontsize = 14), 272 | anNodes = "HECnodes.txt", 273 | anNode.gpar = gpar(col = "black"), 274 | grInfo = "HECgraphics.txt", 275 | arrow = c("more\ncommon", 0.0, 2, 4, 1, -2)) 276 | 277 | grid.text("males", x = 0, y = 2.3, default.units = "native") 278 | grid.text("females", x = 0, y = -2.3, default.units = "native") 279 | grid.text("Pairing of Eye Color with Hair Color", x = 0, y = 3.75, 280 | default.units = "native", gp = gpar(fontsize = 18)) 281 | grid.text("A test of plotHive annotation options", x = 0, y = 3.25, 282 | default.units = "native", gp = gpar(fontsize = 12)) 283 | grid.text("Images from Wikipedia Commons", x = 0, y = -3.5, 284 | default.units = "native", gp = gpar(fontsize = 9)) 285 | setwd(currDir) 286 | } 287 | , height = 20 288 | , width = 30) 289 | ) 290 | ``` 291 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/svg-pan-zoom/dist/svg-pan-zoom.min.js: -------------------------------------------------------------------------------- 1 | // svg-pan-zoom v3.6.1 2 | // https://github.com/ariutta/svg-pan-zoom 3 | !function s(r,a,l){function u(e,t){if(!a[e]){if(!r[e]){var o="function"==typeof require&&require;if(!t&&o)return o(e,!0);if(h)return h(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var i=a[e]={exports:{}};r[e][0].call(i.exports,function(t){return u(r[e][1][t]||t)},i,i.exports,s,r,a,l)}return a[e].exports}for(var h="function"==typeof require&&require,t=0;tthis.options.maxZoom*n.zoom&&(t=this.options.maxZoom*n.zoom/this.getZoom());var i=this.viewport.getCTM(),s=e.matrixTransform(i.inverse()),r=this.svg.createSVGMatrix().translate(s.x,s.y).scale(t).translate(-s.x,-s.y),a=i.multiply(r);a.a!==i.a&&this.viewport.setCTM(a)},i.prototype.zoom=function(t,e){this.zoomAtPoint(t,a.getSvgCenterPoint(this.svg,this.width,this.height),e)},i.prototype.publicZoom=function(t,e){e&&(t=this.computeFromRelativeZoom(t)),this.zoom(t,e)},i.prototype.publicZoomAtPoint=function(t,e,o){if(o&&(t=this.computeFromRelativeZoom(t)),"SVGPoint"!==r.getType(e)){if(!("x"in e&&"y"in e))throw new Error("Given point is invalid");e=a.createSVGPoint(this.svg,e.x,e.y)}this.zoomAtPoint(t,e,o)},i.prototype.getZoom=function(){return this.viewport.getZoom()},i.prototype.getRelativeZoom=function(){return this.viewport.getRelativeZoom()},i.prototype.computeFromRelativeZoom=function(t){return t*this.viewport.getOriginalState().zoom},i.prototype.resetZoom=function(){var t=this.viewport.getOriginalState();this.zoom(t.zoom,!0)},i.prototype.resetPan=function(){this.pan(this.viewport.getOriginalState())},i.prototype.reset=function(){this.resetZoom(),this.resetPan()},i.prototype.handleDblClick=function(t){var e;if((this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),this.options.controlIconsEnabled)&&-1<(t.target.getAttribute("class")||"").indexOf("svg-pan-zoom-control"))return!1;e=t.shiftKey?1/(2*(1+this.options.zoomScaleSensitivity)):2*(1+this.options.zoomScaleSensitivity);var o=a.getEventPoint(t,this.svg).matrixTransform(this.svg.getScreenCTM().inverse());this.zoomAtPoint(e,o)},i.prototype.handleMouseDown=function(t,e){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),r.mouseAndTouchNormalize(t,this.svg),this.options.dblClickZoomEnabled&&r.isDblClick(t,e)?this.handleDblClick(t):(this.state="pan",this.firstEventCTM=this.viewport.getCTM(),this.stateOrigin=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()))},i.prototype.handleMouseMove=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&this.options.panEnabled){var e=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()),o=this.firstEventCTM.translate(e.x-this.stateOrigin.x,e.y-this.stateOrigin.y);this.viewport.setCTM(o)}},i.prototype.handleMouseUp=function(t){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&(this.state="none")},i.prototype.fit=function(){var t=this.viewport.getViewBox(),e=Math.min(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.contain=function(){var t=this.viewport.getViewBox(),e=Math.max(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.center=function(){var t=this.viewport.getViewBox(),e=.5*(this.width-(t.width+2*t.x)*this.getZoom()),o=.5*(this.height-(t.height+2*t.y)*this.getZoom());this.getPublicInstance().pan({x:e,y:o})},i.prototype.updateBBox=function(){this.viewport.simpleViewBoxCache()},i.prototype.pan=function(t){var e=this.viewport.getCTM();e.e=t.x,e.f=t.y,this.viewport.setCTM(e)},i.prototype.panBy=function(t){var e=this.viewport.getCTM();e.e+=t.x,e.f+=t.y,this.viewport.setCTM(e)},i.prototype.getPan=function(){var t=this.viewport.getState();return{x:t.x,y:t.y}},i.prototype.resize=function(){var t=a.getBoundingClientRectNormalized(this.svg);this.width=t.width,this.height=t.height;var e=this.viewport;e.options.width=this.width,e.options.height=this.height,e.processCTM(),this.options.controlIconsEnabled&&(this.getPublicInstance().disableControlIcons(),this.getPublicInstance().enableControlIcons())},i.prototype.destroy=function(){var e=this;for(var t in this.beforeZoom=null,this.onZoom=null,this.beforePan=null,this.onPan=null,(this.onUpdatedCTM=null)!=this.options.customEventsHandler&&this.options.customEventsHandler.destroy({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()}),this.eventListeners)(this.options.eventsListenerElement||this.svg).removeEventListener(t,this.eventListeners[t],!this.options.preventMouseEventsDefault&&h);this.disableMouseWheelZoom(),this.getPublicInstance().disableControlIcons(),this.reset(),c=c.filter(function(t){return t.svg!==e.svg}),delete this.options,delete this.viewport,delete this.publicInstance,delete this.pi,this.getPublicInstance=function(){return null}},i.prototype.getPublicInstance=function(){var o=this;return this.publicInstance||(this.publicInstance=this.pi={enablePan:function(){return o.options.panEnabled=!0,o.pi},disablePan:function(){return o.options.panEnabled=!1,o.pi},isPanEnabled:function(){return!!o.options.panEnabled},pan:function(t){return o.pan(t),o.pi},panBy:function(t){return o.panBy(t),o.pi},getPan:function(){return o.getPan()},setBeforePan:function(t){return o.options.beforePan=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnPan:function(t){return o.options.onPan=null===t?null:r.proxy(t,o.publicInstance),o.pi},enableZoom:function(){return o.options.zoomEnabled=!0,o.pi},disableZoom:function(){return o.options.zoomEnabled=!1,o.pi},isZoomEnabled:function(){return!!o.options.zoomEnabled},enableControlIcons:function(){return o.options.controlIconsEnabled||(o.options.controlIconsEnabled=!0,s.enable(o)),o.pi},disableControlIcons:function(){return o.options.controlIconsEnabled&&(o.options.controlIconsEnabled=!1,s.disable(o)),o.pi},isControlIconsEnabled:function(){return!!o.options.controlIconsEnabled},enableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!0,o.pi},disableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!1,o.pi},isDblClickZoomEnabled:function(){return!!o.options.dblClickZoomEnabled},enableMouseWheelZoom:function(){return o.enableMouseWheelZoom(),o.pi},disableMouseWheelZoom:function(){return o.disableMouseWheelZoom(),o.pi},isMouseWheelZoomEnabled:function(){return!!o.options.mouseWheelZoomEnabled},setZoomScaleSensitivity:function(t){return o.options.zoomScaleSensitivity=t,o.pi},setMinZoom:function(t){return o.options.minZoom=t,o.pi},setMaxZoom:function(t){return o.options.maxZoom=t,o.pi},setBeforeZoom:function(t){return o.options.beforeZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnZoom:function(t){return o.options.onZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},zoom:function(t){return o.publicZoom(t,!0),o.pi},zoomBy:function(t){return o.publicZoom(t,!1),o.pi},zoomAtPoint:function(t,e){return o.publicZoomAtPoint(t,e,!0),o.pi},zoomAtPointBy:function(t,e){return o.publicZoomAtPoint(t,e,!1),o.pi},zoomIn:function(){return this.zoomBy(1+o.options.zoomScaleSensitivity),o.pi},zoomOut:function(){return this.zoomBy(1/(1+o.options.zoomScaleSensitivity)),o.pi},getZoom:function(){return o.getRelativeZoom()},setOnUpdatedCTM:function(t){return o.options.onUpdatedCTM=null===t?null:r.proxy(t,o.publicInstance),o.pi},resetZoom:function(){return o.resetZoom(),o.pi},resetPan:function(){return o.resetPan(),o.pi},reset:function(){return o.reset(),o.pi},fit:function(){return o.fit(),o.pi},contain:function(){return o.contain(),o.pi},center:function(){return o.center(),o.pi},updateBBox:function(){return o.updateBBox(),o.pi},resize:function(){return o.resize(),o.pi},getSizes:function(){return{width:o.width,height:o.height,realZoom:o.getZoom(),viewBox:o.viewport.getViewBox()}},destroy:function(){return o.destroy(),o.pi}}),this.publicInstance};var c=[];e.exports=function(t,e){var o=r.getSvg(t);if(null===o)return null;for(var n=c.length-1;0<=n;n--)if(c[n].svg===o)return c[n].instance.getPublicInstance();return c.push({svg:o,instance:new i(o,e)}),c[c.length-1].instance.getPublicInstance()}},{"./control-icons":1,"./shadow-viewport":2,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(t,e,o){var l=t("./utilities"),s="unknown";document.documentMode&&(s="ie"),e.exports={svgNS:"http://www.w3.org/2000/svg",xmlNS:"http://www.w3.org/XML/1998/namespace",xmlnsNS:"http://www.w3.org/2000/xmlns/",xlinkNS:"http://www.w3.org/1999/xlink",evNS:"http://www.w3.org/2001/xml-events",getBoundingClientRectNormalized:function(t){if(t.clientWidth&&t.clientHeight)return{width:t.clientWidth,height:t.clientHeight};if(t.getBoundingClientRect())return t.getBoundingClientRect();throw new Error("Cannot get BoundingClientRect for SVG.")},getOrCreateViewport:function(t,e){var o=null;if(!(o=l.isElement(e)?e:t.querySelector(e))){var n=Array.prototype.slice.call(t.childNodes||t.children).filter(function(t){return"defs"!==t.nodeName&&"#text"!==t.nodeName});1===n.length&&"g"===n[0].nodeName&&null===n[0].getAttribute("transform")&&(o=n[0])}if(!o){var i="viewport-"+(new Date).toISOString().replace(/\D/g,"");(o=document.createElementNS(this.svgNS,"g")).setAttribute("id",i);var s=t.childNodes||t.children;if(s&&0-1){this.requireFail.splice(index,1)}return this}},{key:"hasRequireFailures",value:function hasRequireFailures(){return this.requireFail.length>0}},{key:"canRecognizeWith",value:function canRecognizeWith(otherRecognizer){return!!this.simultaneous[otherRecognizer.id]}},{key:"emit",value:function emit(input){var self=this;var state=this.state;function emit(event){self.manager.emit(event,input)}if(state=STATE_ENDED){emit(self.options.event+stateStr(state))}}},{key:"tryEmit",value:function tryEmit(input){if(this.canEmit()){return this.emit(input)}this.state=STATE_FAILED}},{key:"canEmit",value:function canEmit(){var i=0;while(ithis.options.threshold||this.state&STATE_BEGAN)}}]);return RotateRecognizer}(AttrRecognizer);RotateRecognizer.prototype.defaults={event:"rotate",threshold:0,pointers:2};var PinchRecognizer=function(_AttrRecognizer){inherits(PinchRecognizer,_AttrRecognizer);function PinchRecognizer(){classCallCheck(this,PinchRecognizer);return possibleConstructorReturn(this,(PinchRecognizer.__proto__||Object.getPrototypeOf(PinchRecognizer)).apply(this,arguments))}createClass(PinchRecognizer,[{key:"getTouchAction",value:function getTouchAction(){return[TOUCH_ACTION_NONE]}},{key:"attrTest",value:function attrTest(input){return get(PinchRecognizer.prototype.__proto__||Object.getPrototypeOf(PinchRecognizer.prototype),"attrTest",this).call(this,input)&&(Math.abs(input.scale-1)>this.options.threshold||this.state&STATE_BEGAN)}},{key:"emit",value:function emit(input){if(input.scale!==1){var inOut=input.scale<1?"in":"out";input.additionalEvent=this.options.event+inOut}get(PinchRecognizer.prototype.__proto__||Object.getPrototypeOf(PinchRecognizer.prototype),"emit",this).call(this,input)}}]);return PinchRecognizer}(AttrRecognizer);PinchRecognizer.prototype.defaults={event:"pinch",threshold:0,pointers:2};function directionStr(direction){if(direction===DIRECTION_DOWN){return"down"}else if(direction===DIRECTION_UP){return"up"}else if(direction===DIRECTION_LEFT){return"left"}else if(direction===DIRECTION_RIGHT){return"right"}return""}var PanRecognizer=function(_AttrRecognizer){inherits(PanRecognizer,_AttrRecognizer);function PanRecognizer(){classCallCheck(this,PanRecognizer);var _this=possibleConstructorReturn(this,(PanRecognizer.__proto__||Object.getPrototypeOf(PanRecognizer)).apply(this,arguments));_this.pX=null;_this.pY=null;return _this}createClass(PanRecognizer,[{key:"getTouchAction",value:function getTouchAction(){var direction=this.options.direction;var actions=[];if(direction&DIRECTION_HORIZONTAL){actions.push(TOUCH_ACTION_PAN_Y)}if(direction&DIRECTION_VERTICAL){actions.push(TOUCH_ACTION_PAN_X)}return actions}},{key:"directionTest",value:function directionTest(input){var options=this.options;var hasMoved=true;var distance=input.distance;var direction=input.direction;var x=input.deltaX;var y=input.deltaY;if(!(direction&options.direction)){if(options.direction&DIRECTION_HORIZONTAL){direction=x===0?DIRECTION_NONE:x<0?DIRECTION_LEFT:DIRECTION_RIGHT;hasMoved=x!==this.pX;distance=Math.abs(input.deltaX)}else{direction=y===0?DIRECTION_NONE:y<0?DIRECTION_UP:DIRECTION_DOWN;hasMoved=y!==this.pY;distance=Math.abs(input.deltaY)}}input.direction=direction;return hasMoved&&distance>options.threshold&&direction&options.direction}},{key:"attrTest",value:function attrTest(input){return AttrRecognizer.prototype.attrTest.call(this,input)&&(this.state&STATE_BEGAN||!(this.state&STATE_BEGAN)&&this.directionTest(input))}},{key:"emit",value:function emit(input){this.pX=input.deltaX;this.pY=input.deltaY;var direction=directionStr(input.direction);if(direction){input.additionalEvent=this.options.event+direction}get(PanRecognizer.prototype.__proto__||Object.getPrototypeOf(PanRecognizer.prototype),"emit",this).call(this,input)}}]);return PanRecognizer}(AttrRecognizer);PanRecognizer.prototype.defaults={event:"pan",threshold:10,pointers:1,direction:DIRECTION_ALL};var SwipeRecognizer=function(_AttrRecognizer){inherits(SwipeRecognizer,_AttrRecognizer);function SwipeRecognizer(){classCallCheck(this,SwipeRecognizer);return possibleConstructorReturn(this,(SwipeRecognizer.__proto__||Object.getPrototypeOf(SwipeRecognizer)).apply(this,arguments))}createClass(SwipeRecognizer,[{key:"getTouchAction",value:function getTouchAction(){return PanRecognizer.prototype.getTouchAction.call(this)}},{key:"attrTest",value:function attrTest(input){var direction=this.options.direction;var velocity=void 0;if(direction&(DIRECTION_HORIZONTAL|DIRECTION_VERTICAL)){velocity=input.overallVelocity}else if(direction&DIRECTION_HORIZONTAL){velocity=input.overallVelocityX}else if(direction&DIRECTION_VERTICAL){velocity=input.overallVelocityY}return get(SwipeRecognizer.prototype.__proto__||Object.getPrototypeOf(SwipeRecognizer.prototype),"attrTest",this).call(this,input)&&direction&input.offsetDirection&&input.distance>this.options.threshold&&input.maxPointers===this.options.pointers&&abs(velocity)>this.options.velocity&&input.eventType&INPUT_END}},{key:"emit",value:function emit(input){var direction=directionStr(input.offsetDirection);if(direction){this.manager.emit(this.options.event+direction,input)}this.manager.emit(this.options.event,input)}}]);return SwipeRecognizer}(AttrRecognizer);SwipeRecognizer.prototype.defaults={event:"swipe",threshold:10,velocity:.3,direction:DIRECTION_HORIZONTAL|DIRECTION_VERTICAL,pointers:1};function bindFn(fn,context){return function boundFn(){return fn.apply(context,arguments)}}function setTimeoutContext(fn,timeout,context){return setTimeout(bindFn(fn,context),timeout)}function getDistance(p1,p2,props){if(!props){props=PROPS_XY}var x=p2[props[0]]-p1[props[0]];var y=p2[props[1]]-p1[props[1]];return Math.sqrt(x*x+y*y)}var TapRecognizer=function(_Recognizer){inherits(TapRecognizer,_Recognizer);function TapRecognizer(){classCallCheck(this,TapRecognizer);var _this=possibleConstructorReturn(this,(TapRecognizer.__proto__||Object.getPrototypeOf(TapRecognizer)).apply(this,arguments));_this.pTime=false;_this.pCenter=false;_this._timer=null;_this._input=null;_this.count=0;return _this}createClass(TapRecognizer,[{key:"getTouchAction",value:function getTouchAction(){return[TOUCH_ACTION_MANIPULATION]}},{key:"process",value:function process(input){var _this2=this;var options=this.options;var validPointers=input.pointers.length===options.pointers;var validMovement=input.distanceoptions.time;this._input=input;if(!validMovement||!validPointers||input.eventType&(INPUT_END|INPUT_CANCEL)&&!validTime){this.reset()}else if(input.eventType&INPUT_START){this.reset();this._timer=setTimeoutContext(function(){_this2.state=STATE_RECOGNIZED;_this2.tryEmit()},options.time,this)}else if(input.eventType&INPUT_END){return STATE_RECOGNIZED}return STATE_FAILED}},{key:"reset",value:function reset(){clearTimeout(this._timer)}},{key:"emit",value:function emit(input){if(this.state!==STATE_RECOGNIZED){return}if(input&&input.eventType&INPUT_END){this.manager.emit(this.options.event+"up",input)}else{this._input.timeStamp=now();this.manager.emit(this.options.event,this._input)}}}]);return PressRecognizer}(Recognizer);PressRecognizer.prototype.defaults={event:"press",pointers:1,time:251,threshold:9};function inStr(str,find){return str.indexOf(find)>-1}function cleanTouchActions(actions){if(inStr(actions,TOUCH_ACTION_NONE)){return TOUCH_ACTION_NONE}var hasPanX=inStr(actions,TOUCH_ACTION_PAN_X);var hasPanY=inStr(actions,TOUCH_ACTION_PAN_Y);if(hasPanX&&hasPanY){return TOUCH_ACTION_NONE}if(hasPanX||hasPanY){return hasPanX?TOUCH_ACTION_PAN_X:TOUCH_ACTION_PAN_Y}if(inStr(actions,TOUCH_ACTION_MANIPULATION)){return TOUCH_ACTION_MANIPULATION}return TOUCH_ACTION_AUTO}var TouchAction=function(){function TouchAction(manager,value){classCallCheck(this,TouchAction);this.manager=manager;this.set(value)}createClass(TouchAction,[{key:"set",value:function set(value){if(value===TOUCH_ACTION_COMPUTE){value=this.compute()}if(NATIVE_TOUCH_ACTION&&this.manager.element.style&&TOUCH_ACTION_MAP[value]){this.manager.element.style[PREFIXED_TOUCH_ACTION]=value}this.actions=value.toLowerCase().trim()}},{key:"update",value:function update(){this.set(this.manager.options.touchAction)}},{key:"compute",value:function compute(){var actions=[];each(this.manager.recognizers,function(recognizer){if(boolOrFn(recognizer.options.enable,[recognizer])){actions=actions.concat(recognizer.getTouchAction())}});return cleanTouchActions(actions.join(" "))}},{key:"preventDefaults",value:function preventDefaults(input){var srcEvent=input.srcEvent;var direction=input.offsetDirection;if(this.manager.session.prevented){srcEvent.preventDefault();return}var actions=this.actions;var hasNone=inStr(actions,TOUCH_ACTION_NONE)&&!TOUCH_ACTION_MAP[TOUCH_ACTION_NONE];var hasPanY=inStr(actions,TOUCH_ACTION_PAN_Y)&&!TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y];var hasPanX=inStr(actions,TOUCH_ACTION_PAN_X)&&!TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X];if(hasNone){var isTapPointer=input.pointers.length===1;var isTapMovement=input.distance<2;var isTapTouchTime=input.deltaTime<250;if(isTapPointer&&isTapMovement&&isTapTouchTime){return}}if(hasPanX&&hasPanY){return}if(hasNone||hasPanY&&direction&DIRECTION_HORIZONTAL||hasPanX&&direction&DIRECTION_VERTICAL){return this.preventSrc(srcEvent)}}},{key:"preventSrc",value:function preventSrc(srcEvent){this.manager.session.prevented=true;srcEvent.preventDefault()}}]);return TouchAction}();function hasParent(node,parent){while(node){if(node===parent){return true}node=node.parentNode}return false}function getCenter(pointers){var pointersLength=pointers.length;if(pointersLength===1){return{x:round(pointers[0].clientX),y:round(pointers[0].clientY)}}var x=0;var y=0;var i=0;while(i=abs(y)){return x<0?DIRECTION_LEFT:DIRECTION_RIGHT}return y<0?DIRECTION_UP:DIRECTION_DOWN}function computeDeltaXY(session,input){var center=input.center;var offset=session.offsetDelta||{};var prevDelta=session.prevDelta||{};var prevInput=session.prevInput||{};if(input.eventType===INPUT_START||prevInput.eventType===INPUT_END){prevDelta=session.prevDelta={x:prevInput.deltaX||0,y:prevInput.deltaY||0};offset=session.offsetDelta={x:center.x,y:center.y}}input.deltaX=prevDelta.x+(center.x-offset.x);input.deltaY=prevDelta.y+(center.y-offset.y)}function getVelocity(deltaTime,x,y){return{x:x/deltaTime||0,y:y/deltaTime||0}}function getScale(start,end){return getDistance(end[0],end[1],PROPS_CLIENT_XY)/getDistance(start[0],start[1],PROPS_CLIENT_XY)}function getRotation(start,end){return getAngle(end[1],end[0],PROPS_CLIENT_XY)+getAngle(start[1],start[0],PROPS_CLIENT_XY)}function computeIntervalInputData(session,input){var last=session.lastInterval||input;var deltaTime=input.timeStamp-last.timeStamp;var velocity=void 0;var velocityX=void 0;var velocityY=void 0;var direction=void 0;if(input.eventType!==INPUT_CANCEL&&(deltaTime>COMPUTE_INTERVAL||last.velocity===undefined)){var deltaX=input.deltaX-last.deltaX;var deltaY=input.deltaY-last.deltaY;var v=getVelocity(deltaTime,deltaX,deltaY);velocityX=v.x;velocityY=v.y;velocity=abs(v.x)>abs(v.y)?v.x:v.y;direction=getDirection(deltaX,deltaY);session.lastInterval=input}else{velocity=last.velocity;velocityX=last.velocityX;velocityY=last.velocityY;direction=last.direction}input.velocity=velocity;input.velocityX=velocityX;input.velocityY=velocityY;input.direction=direction}function computeInputData(manager,input){var session=manager.session;var pointers=input.pointers;var pointersLength=pointers.length;if(!session.firstInput){session.firstInput=simpleCloneInputData(input)}if(pointersLength>1&&!session.firstMultiple){session.firstMultiple=simpleCloneInputData(input)}else if(pointersLength===1){session.firstMultiple=false}var firstInput=session.firstInput;var firstMultiple=session.firstMultiple;var offsetCenter=firstMultiple?firstMultiple.center:firstInput.center;var center=input.center=getCenter(pointers);input.timeStamp=now();input.deltaTime=input.timeStamp-firstInput.timeStamp;input.angle=getAngle(offsetCenter,center);input.distance=getDistance(offsetCenter,center);computeDeltaXY(session,input);input.offsetDirection=getDirection(input.deltaX,input.deltaY);var overallVelocity=getVelocity(input.deltaTime,input.deltaX,input.deltaY);input.overallVelocityX=overallVelocity.x;input.overallVelocityY=overallVelocity.y;input.overallVelocity=abs(overallVelocity.x)>abs(overallVelocity.y)?overallVelocity.x:overallVelocity.y;input.scale=firstMultiple?getScale(firstMultiple.pointers,pointers):1;input.rotation=firstMultiple?getRotation(firstMultiple.pointers,pointers):0;input.maxPointers=!session.prevInput?input.pointers.length:input.pointers.length>session.prevInput.maxPointers?input.pointers.length:session.prevInput.maxPointers;computeIntervalInputData(session,input);var target=manager.element;if(hasParent(input.srcEvent.target,target)){target=input.srcEvent.target}input.target=target}function inputHandler(manager,eventType,input){var pointersLen=input.pointers.length;var changedPointersLen=input.changedPointers.length;var isFirst=eventType&INPUT_START&&pointersLen-changedPointersLen===0;var isFinal=eventType&(INPUT_END|INPUT_CANCEL)&&pointersLen-changedPointersLen===0;input.isFirst=!!isFirst;input.isFinal=!!isFinal;if(isFirst){manager.session={}}input.eventType=eventType;computeInputData(manager,input);manager.emit("hammer.input",input);manager.recognize(input);manager.session.prevInput=input}function splitStr(str){return str.trim().split(/\s+/g)}function addEventListeners(target,types,handler){each(splitStr(types),function(type){target.addEventListener(type,handler,false)})}function removeEventListeners(target,types,handler){each(splitStr(types),function(type){target.removeEventListener(type,handler,false)})}function getWindowForElement(element){var doc=element.ownerDocument||element;return doc.defaultView||doc.parentWindow||window}var Input=function(){function Input(manager,callback){classCallCheck(this,Input);var self=this;this.manager=manager;this.callback=callback;this.element=manager.element;this.target=manager.options.inputTarget;this.domHandler=function(ev){if(boolOrFn(manager.options.enable,[manager])){self.handler(ev)}};this.init()}createClass(Input,[{key:"handler",value:function handler(){}},{key:"init",value:function init(){this.evEl&&addEventListeners(this.element,this.evEl,this.domHandler);this.evTarget&&addEventListeners(this.target,this.evTarget,this.domHandler);this.evWin&&addEventListeners(getWindowForElement(this.element),this.evWin,this.domHandler)}},{key:"destroy",value:function destroy(){this.evEl&&removeEventListeners(this.element,this.evEl,this.domHandler);this.evTarget&&removeEventListeners(this.target,this.evTarget,this.domHandler);this.evWin&&removeEventListeners(getWindowForElement(this.element),this.evWin,this.domHandler)}}]);return Input}();var POINTER_INPUT_MAP={pointerdown:INPUT_START,pointermove:INPUT_MOVE,pointerup:INPUT_END,pointercancel:INPUT_CANCEL,pointerout:INPUT_CANCEL};var IE10_POINTER_TYPE_ENUM={2:INPUT_TYPE_TOUCH,3:INPUT_TYPE_PEN,4:INPUT_TYPE_MOUSE,5:INPUT_TYPE_KINECT};var POINTER_ELEMENT_EVENTS="pointerdown";var POINTER_WINDOW_EVENTS="pointermove pointerup pointercancel";if(window.MSPointerEvent&&!window.PointerEvent){POINTER_ELEMENT_EVENTS="MSPointerDown";POINTER_WINDOW_EVENTS="MSPointerMove MSPointerUp MSPointerCancel"}var PointerEventInput=function(_Input){inherits(PointerEventInput,_Input);function PointerEventInput(){classCallCheck(this,PointerEventInput);var _this=possibleConstructorReturn(this,(PointerEventInput.__proto__||Object.getPrototypeOf(PointerEventInput)).apply(this,arguments));_this.evEl=POINTER_ELEMENT_EVENTS;_this.evWin=POINTER_WINDOW_EVENTS;_this.store=_this.manager.session.pointerEvents=[];return _this}createClass(PointerEventInput,[{key:"handler",value:function handler(ev){var store=this.store;var removePointer=false;var eventTypeNormalized=ev.type.toLowerCase().replace("ms","");var eventType=POINTER_INPUT_MAP[eventTypeNormalized];var pointerType=IE10_POINTER_TYPE_ENUM[ev.pointerType]||ev.pointerType;var isTouch=pointerType===INPUT_TYPE_TOUCH;var storeIndex=inArray(store,ev.pointerId,"pointerId");if(eventType&INPUT_START&&(ev.button===0||isTouch)){if(storeIndex<0){store.push(ev);storeIndex=store.length-1}}else if(eventType&(INPUT_END|INPUT_CANCEL)){removePointer=true}if(storeIndex<0){return}store[storeIndex]=ev;this.callback(this.manager,eventType,{pointers:store,changedPointers:[ev],pointerType:pointerType,srcEvent:ev});if(removePointer){store.splice(storeIndex,1)}}}]);return PointerEventInput}(Input);function toArray$1(obj){return Array.prototype.slice.call(obj,0)}function uniqueArray(src,key,sort){var results=[];var values=[];var i=0;while(ib[key]})}}return results}var TOUCH_INPUT_MAP={touchstart:INPUT_START,touchmove:INPUT_MOVE,touchend:INPUT_END,touchcancel:INPUT_CANCEL};var TOUCH_TARGET_EVENTS="touchstart touchmove touchend touchcancel";var TouchInput=function(_Input){inherits(TouchInput,_Input);function TouchInput(){classCallCheck(this,TouchInput);TouchInput.prototype.evTarget=TOUCH_TARGET_EVENTS; 7 | TouchInput.prototype.targetIds={};var _this=possibleConstructorReturn(this,(TouchInput.__proto__||Object.getPrototypeOf(TouchInput)).apply(this,arguments));_this.evTarget=TOUCH_TARGET_EVENTS;_this.targetIds={};return _this}createClass(TouchInput,[{key:"handler",value:function handler(ev){var type=TOUCH_INPUT_MAP[ev.type];var touches=getTouches.call(this,ev,type);if(!touches){return}this.callback(this.manager,type,{pointers:touches[0],changedPointers:touches[1],pointerType:INPUT_TYPE_TOUCH,srcEvent:ev})}}]);return TouchInput}(Input);function getTouches(ev,type){var allTouches=toArray$1(ev.touches);var targetIds=this.targetIds;if(type&(INPUT_START|INPUT_MOVE)&&allTouches.length===1){targetIds[allTouches[0].identifier]=true;return[allTouches,allTouches]}var i=void 0;var targetTouches=void 0;var changedTouches=toArray$1(ev.changedTouches);var changedTargetTouches=[];var target=this.target;targetTouches=allTouches.filter(function(touch){return hasParent(touch.target,target)});if(type===INPUT_START){i=0;while(i-1){lts.splice(i,1)}};setTimeout(removeLastTouch,DEDUP_TIMEOUT)})()}}function isSyntheticEvent(eventData){var x=eventData.srcEvent.clientX;var y=eventData.srcEvent.clientY;for(var i=0;i\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace";var log=window.console&&(window.console.warn||window.console.log);if(log){log.call(window.console,deprecationMessage,stack)}return method.apply(this,arguments)}}var extend=deprecate(function(dest,src,merge){var keys=Object.keys(src);var i=0;while(i= 0; i--) { 635 | if (this.eventListeners.hasOwnProperty(haltEventListeners[i])) { 636 | delete this.eventListeners[haltEventListeners[i]] 637 | } 638 | } 639 | } 640 | } 641 | 642 | // Bind eventListeners 643 | for (var event in this.eventListeners) { 644 | this.svg.addEventListener(event, this.eventListeners[event], false) 645 | } 646 | 647 | // Zoom using mouse wheel 648 | if (this.options.mouseWheelZoomEnabled) { 649 | this.options.mouseWheelZoomEnabled = false // set to false as enable will set it back to true 650 | this.enableMouseWheelZoom() 651 | } 652 | } 653 | 654 | /** 655 | * Enable ability to zoom using mouse wheel 656 | */ 657 | SvgPanZoom.prototype.enableMouseWheelZoom = function() { 658 | if (!this.options.mouseWheelZoomEnabled) { 659 | var that = this 660 | 661 | // Mouse wheel listener 662 | this.wheelListener = function(evt) { 663 | return that.handleMouseWheel(evt); 664 | } 665 | 666 | // Bind wheelListener 667 | Wheel.on(this.svg, this.wheelListener, false) 668 | 669 | this.options.mouseWheelZoomEnabled = true 670 | } 671 | } 672 | 673 | /** 674 | * Disable ability to zoom using mouse wheel 675 | */ 676 | SvgPanZoom.prototype.disableMouseWheelZoom = function() { 677 | if (this.options.mouseWheelZoomEnabled) { 678 | Wheel.off(this.svg, this.wheelListener, false) 679 | this.options.mouseWheelZoomEnabled = false 680 | } 681 | } 682 | 683 | /** 684 | * Handle mouse wheel event 685 | * 686 | * @param {Event} evt 687 | */ 688 | SvgPanZoom.prototype.handleMouseWheel = function(evt) { 689 | if (!this.options.zoomEnabled || this.state !== 'none') { 690 | return; 691 | } 692 | 693 | if (this.options.preventMouseEventsDefault){ 694 | if (evt.preventDefault) { 695 | evt.preventDefault(); 696 | } else { 697 | evt.returnValue = false; 698 | } 699 | } 700 | 701 | // Default delta in case that deltaY is not available 702 | var delta = evt.deltaY || 1 703 | , timeDelta = Date.now() - this.lastMouseWheelEventTime 704 | , divider = 3 + Math.max(0, 30 - timeDelta) 705 | 706 | // Update cache 707 | this.lastMouseWheelEventTime = Date.now() 708 | 709 | // Make empirical adjustments for browsers that give deltaY in pixels (deltaMode=0) 710 | if ('deltaMode' in evt && evt.deltaMode === 0 && evt.wheelDelta) { 711 | delta = evt.deltaY === 0 ? 0 : Math.abs(evt.wheelDelta) / evt.deltaY 712 | } 713 | 714 | delta = -0.3 < delta && delta < 0.3 ? delta : (delta > 0 ? 1 : -1) * Math.log(Math.abs(delta) + 10) / divider 715 | 716 | var inversedScreenCTM = this.svg.getScreenCTM().inverse() 717 | , relativeMousePoint = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(inversedScreenCTM) 718 | , zoom = Math.pow(1 + this.options.zoomScaleSensitivity, (-1) * delta); // multiplying by neg. 1 so as to make zoom in/out behavior match Google maps behavior 719 | 720 | this.zoomAtPoint(zoom, relativeMousePoint) 721 | } 722 | 723 | /** 724 | * Zoom in at a SVG point 725 | * 726 | * @param {SVGPoint} point 727 | * @param {Float} zoomScale Number representing how much to zoom 728 | * @param {Boolean} zoomAbsolute Default false. If true, zoomScale is treated as an absolute value. 729 | * Otherwise, zoomScale is treated as a multiplied (e.g. 1.10 would zoom in 10%) 730 | */ 731 | SvgPanZoom.prototype.zoomAtPoint = function(zoomScale, point, zoomAbsolute) { 732 | var originalState = this.viewport.getOriginalState() 733 | 734 | if (!zoomAbsolute) { 735 | // Fit zoomScale in set bounds 736 | if (this.getZoom() * zoomScale < this.options.minZoom * originalState.zoom) { 737 | zoomScale = (this.options.minZoom * originalState.zoom) / this.getZoom() 738 | } else if (this.getZoom() * zoomScale > this.options.maxZoom * originalState.zoom) { 739 | zoomScale = (this.options.maxZoom * originalState.zoom) / this.getZoom() 740 | } 741 | } else { 742 | // Fit zoomScale in set bounds 743 | zoomScale = Math.max(this.options.minZoom * originalState.zoom, Math.min(this.options.maxZoom * originalState.zoom, zoomScale)) 744 | // Find relative scale to achieve desired scale 745 | zoomScale = zoomScale/this.getZoom() 746 | } 747 | 748 | var oldCTM = this.viewport.getCTM() 749 | , relativePoint = point.matrixTransform(oldCTM.inverse()) 750 | , modifier = this.svg.createSVGMatrix().translate(relativePoint.x, relativePoint.y).scale(zoomScale).translate(-relativePoint.x, -relativePoint.y) 751 | , newCTM = oldCTM.multiply(modifier) 752 | 753 | if (newCTM.a !== oldCTM.a) { 754 | this.viewport.setCTM(newCTM) 755 | } 756 | } 757 | 758 | /** 759 | * Zoom at center point 760 | * 761 | * @param {Float} scale 762 | * @param {Boolean} absolute Marks zoom scale as relative or absolute 763 | */ 764 | SvgPanZoom.prototype.zoom = function(scale, absolute) { 765 | this.zoomAtPoint(scale, SvgUtils.getSvgCenterPoint(this.svg, this.width, this.height), absolute) 766 | } 767 | 768 | /** 769 | * Zoom used by public instance 770 | * 771 | * @param {Float} scale 772 | * @param {Boolean} absolute Marks zoom scale as relative or absolute 773 | */ 774 | SvgPanZoom.prototype.publicZoom = function(scale, absolute) { 775 | if (absolute) { 776 | scale = this.computeFromRelativeZoom(scale) 777 | } 778 | 779 | this.zoom(scale, absolute) 780 | } 781 | 782 | /** 783 | * Zoom at point used by public instance 784 | * 785 | * @param {Float} scale 786 | * @param {SVGPoint|Object} point An object that has x and y attributes 787 | * @param {Boolean} absolute Marks zoom scale as relative or absolute 788 | */ 789 | SvgPanZoom.prototype.publicZoomAtPoint = function(scale, point, absolute) { 790 | if (absolute) { 791 | // Transform zoom into a relative value 792 | scale = this.computeFromRelativeZoom(scale) 793 | } 794 | 795 | // If not a SVGPoint but has x and y than create a SVGPoint 796 | if (Utils.getType(point) !== 'SVGPoint' && 'x' in point && 'y' in point) { 797 | point = SvgUtils.createSVGPoint(this.svg, point.x, point.y) 798 | } else { 799 | throw new Error('Given point is invalid') 800 | return 801 | } 802 | 803 | this.zoomAtPoint(scale, point, absolute) 804 | } 805 | 806 | /** 807 | * Get zoom scale 808 | * 809 | * @return {Float} zoom scale 810 | */ 811 | SvgPanZoom.prototype.getZoom = function() { 812 | return this.viewport.getZoom() 813 | } 814 | 815 | /** 816 | * Get zoom scale for public usage 817 | * 818 | * @return {Float} zoom scale 819 | */ 820 | SvgPanZoom.prototype.getRelativeZoom = function() { 821 | return this.viewport.getRelativeZoom() 822 | } 823 | 824 | /** 825 | * Compute actual zoom from public zoom 826 | * 827 | * @param {Float} zoom 828 | * @return {Float} zoom scale 829 | */ 830 | SvgPanZoom.prototype.computeFromRelativeZoom = function(zoom) { 831 | return zoom * this.viewport.getOriginalState().zoom 832 | } 833 | 834 | /** 835 | * Set zoom to initial state 836 | */ 837 | SvgPanZoom.prototype.resetZoom = function() { 838 | var originalState = this.viewport.getOriginalState() 839 | 840 | this.zoom(originalState.zoom, true); 841 | } 842 | 843 | /** 844 | * Set pan to initial state 845 | */ 846 | SvgPanZoom.prototype.resetPan = function() { 847 | this.pan(this.viewport.getOriginalState()); 848 | } 849 | 850 | /** 851 | * Set pan and zoom to initial state 852 | */ 853 | SvgPanZoom.prototype.reset = function() { 854 | this.resetZoom() 855 | this.resetPan() 856 | } 857 | 858 | /** 859 | * Handle double click event 860 | * See handleMouseDown() for alternate detection method 861 | * 862 | * @param {Event} evt 863 | */ 864 | SvgPanZoom.prototype.handleDblClick = function(evt) { 865 | if (this.options.preventMouseEventsDefault) { 866 | if (evt.preventDefault) { 867 | evt.preventDefault() 868 | } else { 869 | evt.returnValue = false 870 | } 871 | } 872 | 873 | // Check if target was a control button 874 | if (this.options.controlIconsEnabled) { 875 | var targetClass = evt.target.getAttribute('class') || '' 876 | if (targetClass.indexOf('svg-pan-zoom-control') > -1) { 877 | return false 878 | } 879 | } 880 | 881 | var zoomFactor 882 | 883 | if (evt.shiftKey) { 884 | zoomFactor = 1/((1 + this.options.zoomScaleSensitivity) * 2) // zoom out when shift key pressed 885 | } else { 886 | zoomFactor = (1 + this.options.zoomScaleSensitivity) * 2 887 | } 888 | 889 | var point = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.svg.getScreenCTM().inverse()) 890 | this.zoomAtPoint(zoomFactor, point) 891 | } 892 | 893 | /** 894 | * Handle click event 895 | * 896 | * @param {Event} evt 897 | */ 898 | SvgPanZoom.prototype.handleMouseDown = function(evt, prevEvt) { 899 | if (this.options.preventMouseEventsDefault) { 900 | if (evt.preventDefault) { 901 | evt.preventDefault() 902 | } else { 903 | evt.returnValue = false 904 | } 905 | } 906 | 907 | Utils.mouseAndTouchNormalize(evt, this.svg) 908 | 909 | // Double click detection; more consistent than ondblclick 910 | if (this.options.dblClickZoomEnabled && Utils.isDblClick(evt, prevEvt)){ 911 | this.handleDblClick(evt) 912 | } else { 913 | // Pan mode 914 | this.state = 'pan' 915 | this.firstEventCTM = this.viewport.getCTM() 916 | this.stateOrigin = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.firstEventCTM.inverse()) 917 | } 918 | } 919 | 920 | /** 921 | * Handle mouse move event 922 | * 923 | * @param {Event} evt 924 | */ 925 | SvgPanZoom.prototype.handleMouseMove = function(evt) { 926 | if (this.options.preventMouseEventsDefault) { 927 | if (evt.preventDefault) { 928 | evt.preventDefault() 929 | } else { 930 | evt.returnValue = false 931 | } 932 | } 933 | 934 | if (this.state === 'pan' && this.options.panEnabled) { 935 | // Pan mode 936 | var point = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.firstEventCTM.inverse()) 937 | , viewportCTM = this.firstEventCTM.translate(point.x - this.stateOrigin.x, point.y - this.stateOrigin.y) 938 | 939 | this.viewport.setCTM(viewportCTM) 940 | } 941 | } 942 | 943 | /** 944 | * Handle mouse button release event 945 | * 946 | * @param {Event} evt 947 | */ 948 | SvgPanZoom.prototype.handleMouseUp = function(evt) { 949 | if (this.options.preventMouseEventsDefault) { 950 | if (evt.preventDefault) { 951 | evt.preventDefault() 952 | } else { 953 | evt.returnValue = false 954 | } 955 | } 956 | 957 | if (this.state === 'pan') { 958 | // Quit pan mode 959 | this.state = 'none' 960 | } 961 | } 962 | 963 | /** 964 | * Adjust viewport size (only) so it will fit in SVG 965 | * Does not center image 966 | */ 967 | SvgPanZoom.prototype.fit = function() { 968 | var viewBox = this.viewport.getViewBox() 969 | , newScale = Math.min(this.width/viewBox.width, this.height/viewBox.height) 970 | 971 | this.zoom(newScale, true) 972 | } 973 | 974 | /** 975 | * Adjust viewport size (only) so it will contain the SVG 976 | * Does not center image 977 | */ 978 | SvgPanZoom.prototype.contain = function() { 979 | var viewBox = this.viewport.getViewBox() 980 | , newScale = Math.max(this.width/viewBox.width, this.height/viewBox.height) 981 | 982 | this.zoom(newScale, true) 983 | } 984 | 985 | /** 986 | * Adjust viewport pan (only) so it will be centered in SVG 987 | * Does not zoom/fit/contain image 988 | */ 989 | SvgPanZoom.prototype.center = function() { 990 | var viewBox = this.viewport.getViewBox() 991 | , offsetX = (this.width - (viewBox.width + viewBox.x * 2) * this.getZoom()) * 0.5 992 | , offsetY = (this.height - (viewBox.height + viewBox.y * 2) * this.getZoom()) * 0.5 993 | 994 | this.getPublicInstance().pan({x: offsetX, y: offsetY}) 995 | } 996 | 997 | /** 998 | * Update content cached BorderBox 999 | * Use when viewport contents change 1000 | */ 1001 | SvgPanZoom.prototype.updateBBox = function() { 1002 | this.viewport.recacheViewBox() 1003 | } 1004 | 1005 | /** 1006 | * Pan to a rendered position 1007 | * 1008 | * @param {Object} point {x: 0, y: 0} 1009 | */ 1010 | SvgPanZoom.prototype.pan = function(point) { 1011 | var viewportCTM = this.viewport.getCTM() 1012 | viewportCTM.e = point.x 1013 | viewportCTM.f = point.y 1014 | this.viewport.setCTM(viewportCTM) 1015 | } 1016 | 1017 | /** 1018 | * Relatively pan the graph by a specified rendered position vector 1019 | * 1020 | * @param {Object} point {x: 0, y: 0} 1021 | */ 1022 | SvgPanZoom.prototype.panBy = function(point) { 1023 | var viewportCTM = this.viewport.getCTM() 1024 | viewportCTM.e += point.x 1025 | viewportCTM.f += point.y 1026 | this.viewport.setCTM(viewportCTM) 1027 | } 1028 | 1029 | /** 1030 | * Get pan vector 1031 | * 1032 | * @return {Object} {x: 0, y: 0} 1033 | */ 1034 | SvgPanZoom.prototype.getPan = function() { 1035 | var state = this.viewport.getState() 1036 | 1037 | return {x: state.x, y: state.y} 1038 | } 1039 | 1040 | /** 1041 | * Recalculates cached svg dimensions and controls position 1042 | */ 1043 | SvgPanZoom.prototype.resize = function() { 1044 | // Get dimensions 1045 | var boundingClientRectNormalized = SvgUtils.getBoundingClientRectNormalized(this.svg) 1046 | this.width = boundingClientRectNormalized.width 1047 | this.height = boundingClientRectNormalized.height 1048 | 1049 | // Reposition control icons by re-enabling them 1050 | if (this.options.controlIconsEnabled) { 1051 | this.getPublicInstance().disableControlIcons() 1052 | this.getPublicInstance().enableControlIcons() 1053 | } 1054 | } 1055 | 1056 | /** 1057 | * Unbind mouse events, free callbacks and destroy public instance 1058 | */ 1059 | SvgPanZoom.prototype.destroy = function() { 1060 | var that = this 1061 | 1062 | // Free callbacks 1063 | this.beforeZoom = null 1064 | this.onZoom = null 1065 | this.beforePan = null 1066 | this.onPan = null 1067 | 1068 | // Destroy custom event handlers 1069 | if (this.options.customEventsHandler != null) { // jshint ignore:line 1070 | this.options.customEventsHandler.destroy({ 1071 | svgElement: this.svg 1072 | , instance: this.getPublicInstance() 1073 | }) 1074 | } 1075 | 1076 | // Unbind eventListeners 1077 | for (var event in this.eventListeners) { 1078 | this.svg.removeEventListener(event, this.eventListeners[event], false) 1079 | } 1080 | 1081 | // Unbind wheelListener 1082 | this.disableMouseWheelZoom() 1083 | 1084 | // Remove control icons 1085 | this.getPublicInstance().disableControlIcons() 1086 | 1087 | // Reset zoom and pan 1088 | this.reset() 1089 | 1090 | // Remove instance from instancesStore 1091 | instancesStore = instancesStore.filter(function(instance){ 1092 | return instance.svg !== that.svg 1093 | }) 1094 | 1095 | // Delete options and its contents 1096 | delete this.options 1097 | 1098 | // Destroy public instance and rewrite getPublicInstance 1099 | delete this.publicInstance 1100 | delete this.pi 1101 | this.getPublicInstance = function(){ 1102 | return null 1103 | } 1104 | } 1105 | 1106 | /** 1107 | * Returns a public instance object 1108 | * 1109 | * @return {Object} Public instance object 1110 | */ 1111 | SvgPanZoom.prototype.getPublicInstance = function() { 1112 | var that = this 1113 | 1114 | // Create cache 1115 | if (!this.publicInstance) { 1116 | this.publicInstance = this.pi = { 1117 | // Pan 1118 | enablePan: function() {that.options.panEnabled = true; return that.pi} 1119 | , disablePan: function() {that.options.panEnabled = false; return that.pi} 1120 | , isPanEnabled: function() {return !!that.options.panEnabled} 1121 | , pan: function(point) {that.pan(point); return that.pi} 1122 | , panBy: function(point) {that.panBy(point); return that.pi} 1123 | , getPan: function() {return that.getPan()} 1124 | // Pan event 1125 | , setBeforePan: function(fn) {that.options.beforePan = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi} 1126 | , setOnPan: function(fn) {that.options.onPan = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi} 1127 | // Zoom and Control Icons 1128 | , enableZoom: function() {that.options.zoomEnabled = true; return that.pi} 1129 | , disableZoom: function() {that.options.zoomEnabled = false; return that.pi} 1130 | , isZoomEnabled: function() {return !!that.options.zoomEnabled} 1131 | , enableControlIcons: function() { 1132 | if (!that.options.controlIconsEnabled) { 1133 | that.options.controlIconsEnabled = true 1134 | ControlIcons.enable(that) 1135 | } 1136 | return that.pi 1137 | } 1138 | , disableControlIcons: function() { 1139 | if (that.options.controlIconsEnabled) { 1140 | that.options.controlIconsEnabled = false; 1141 | ControlIcons.disable(that) 1142 | } 1143 | return that.pi 1144 | } 1145 | , isControlIconsEnabled: function() {return !!that.options.controlIconsEnabled} 1146 | // Double click zoom 1147 | , enableDblClickZoom: function() {that.options.dblClickZoomEnabled = true; return that.pi} 1148 | , disableDblClickZoom: function() {that.options.dblClickZoomEnabled = false; return that.pi} 1149 | , isDblClickZoomEnabled: function() {return !!that.options.dblClickZoomEnabled} 1150 | // Mouse wheel zoom 1151 | , enableMouseWheelZoom: function() {that.enableMouseWheelZoom(); return that.pi} 1152 | , disableMouseWheelZoom: function() {that.disableMouseWheelZoom(); return that.pi} 1153 | , isMouseWheelZoomEnabled: function() {return !!that.options.mouseWheelZoomEnabled} 1154 | // Zoom scale and bounds 1155 | , setZoomScaleSensitivity: function(scale) {that.options.zoomScaleSensitivity = scale; return that.pi} 1156 | , setMinZoom: function(zoom) {that.options.minZoom = zoom; return that.pi} 1157 | , setMaxZoom: function(zoom) {that.options.maxZoom = zoom; return that.pi} 1158 | // Zoom event 1159 | , setBeforeZoom: function(fn) {that.options.beforeZoom = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi} 1160 | , setOnZoom: function(fn) {that.options.onZoom = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi} 1161 | // Zooming 1162 | , zoom: function(scale) {that.publicZoom(scale, true); return that.pi} 1163 | , zoomBy: function(scale) {that.publicZoom(scale, false); return that.pi} 1164 | , zoomAtPoint: function(scale, point) {that.publicZoomAtPoint(scale, point, true); return that.pi} 1165 | , zoomAtPointBy: function(scale, point) {that.publicZoomAtPoint(scale, point, false); return that.pi} 1166 | , zoomIn: function() {this.zoomBy(1 + that.options.zoomScaleSensitivity); return that.pi} 1167 | , zoomOut: function() {this.zoomBy(1 / (1 + that.options.zoomScaleSensitivity)); return that.pi} 1168 | , getZoom: function() {return that.getRelativeZoom()} 1169 | // Reset 1170 | , resetZoom: function() {that.resetZoom(); return that.pi} 1171 | , resetPan: function() {that.resetPan(); return that.pi} 1172 | , reset: function() {that.reset(); return that.pi} 1173 | // Fit, Contain and Center 1174 | , fit: function() {that.fit(); return that.pi} 1175 | , contain: function() {that.contain(); return that.pi} 1176 | , center: function() {that.center(); return that.pi} 1177 | // Size and Resize 1178 | , updateBBox: function() {that.updateBBox(); return that.pi} 1179 | , resize: function() {that.resize(); return that.pi} 1180 | , getSizes: function() { 1181 | return { 1182 | width: that.width 1183 | , height: that.height 1184 | , realZoom: that.getZoom() 1185 | , viewBox: that.viewport.getViewBox() 1186 | } 1187 | } 1188 | // Destroy 1189 | , destroy: function() {that.destroy(); return that.pi} 1190 | } 1191 | } 1192 | 1193 | return this.publicInstance 1194 | } 1195 | 1196 | /** 1197 | * Stores pairs of instances of SvgPanZoom and SVG 1198 | * Each pair is represented by an object {svg: SVGSVGElement, instance: SvgPanZoom} 1199 | * 1200 | * @type {Array} 1201 | */ 1202 | var instancesStore = [] 1203 | 1204 | var svgPanZoom = function(elementOrSelector, options){ 1205 | var svg = Utils.getSvg(elementOrSelector) 1206 | 1207 | if (svg === null) { 1208 | return null 1209 | } else { 1210 | // Look for existent instance 1211 | for(var i = instancesStore.length - 1; i >= 0; i--) { 1212 | if (instancesStore[i].svg === svg) { 1213 | return instancesStore[i].instance.getPublicInstance() 1214 | } 1215 | } 1216 | 1217 | // If instance not found - create one 1218 | instancesStore.push({ 1219 | svg: svg 1220 | , instance: new SvgPanZoom(svg, options) 1221 | }) 1222 | 1223 | // Return just pushed instance 1224 | return instancesStore[instancesStore.length - 1].instance.getPublicInstance() 1225 | } 1226 | } 1227 | 1228 | module.exports = svgPanZoom; 1229 | 1230 | },{"./control-icons":2,"./shadow-viewport":3,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(require,module,exports){ 1231 | var Utils = require('./utilities') 1232 | , _browser = 'unknown' 1233 | ; 1234 | 1235 | // http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser 1236 | if (/*@cc_on!@*/false || !!document.documentMode) { // internet explorer 1237 | _browser = 'ie'; 1238 | } 1239 | 1240 | module.exports = { 1241 | svgNS: 'http://www.w3.org/2000/svg' 1242 | , xmlNS: 'http://www.w3.org/XML/1998/namespace' 1243 | , xmlnsNS: 'http://www.w3.org/2000/xmlns/' 1244 | , xlinkNS: 'http://www.w3.org/1999/xlink' 1245 | , evNS: 'http://www.w3.org/2001/xml-events' 1246 | 1247 | /** 1248 | * Get svg dimensions: width and height 1249 | * 1250 | * @param {SVGSVGElement} svg 1251 | * @return {Object} {width: 0, height: 0} 1252 | */ 1253 | , getBoundingClientRectNormalized: function(svg) { 1254 | if (svg.clientWidth && svg.clientHeight) { 1255 | return {width: svg.clientWidth, height: svg.clientHeight} 1256 | } else if (!!svg.getBoundingClientRect()) { 1257 | return svg.getBoundingClientRect(); 1258 | } else { 1259 | throw new Error('Cannot get BoundingClientRect for SVG.'); 1260 | } 1261 | } 1262 | 1263 | /** 1264 | * Gets g element with class of "viewport" or creates it if it doesn't exist 1265 | * 1266 | * @param {SVGSVGElement} svg 1267 | * @return {SVGElement} g (group) element 1268 | */ 1269 | , getOrCreateViewport: function(svg, selector) { 1270 | var viewport = null 1271 | 1272 | if (Utils.isElement(selector)) { 1273 | viewport = selector 1274 | } else { 1275 | viewport = svg.querySelector(selector) 1276 | } 1277 | 1278 | // Check if there is just one main group in SVG 1279 | if (!viewport) { 1280 | var childNodes = Array.prototype.slice.call(svg.childNodes || svg.children).filter(function(el){ 1281 | return el.nodeName !== 'defs' && el.nodeName !== '#text' 1282 | }) 1283 | 1284 | // Node name should be SVGGElement and should have no transform attribute 1285 | // Groups with transform are not used as viewport because it involves parsing of all transform possibilities 1286 | if (childNodes.length === 1 && childNodes[0].nodeName === 'g' && childNodes[0].getAttribute('transform') === null) { 1287 | viewport = childNodes[0] 1288 | } 1289 | } 1290 | 1291 | // If no favorable group element exists then create one 1292 | if (!viewport) { 1293 | var viewportId = 'viewport-' + new Date().toISOString().replace(/\D/g, ''); 1294 | viewport = document.createElementNS(this.svgNS, 'g'); 1295 | viewport.setAttribute('id', viewportId); 1296 | 1297 | // Internet Explorer (all versions?) can't use childNodes, but other browsers prefer (require?) using childNodes 1298 | var svgChildren = svg.childNodes || svg.children; 1299 | if (!!svgChildren && svgChildren.length > 0) { 1300 | for (var i = svgChildren.length; i > 0; i--) { 1301 | // Move everything into viewport except defs 1302 | if (svgChildren[svgChildren.length - i].nodeName !== 'defs') { 1303 | viewport.appendChild(svgChildren[svgChildren.length - i]); 1304 | } 1305 | } 1306 | } 1307 | svg.appendChild(viewport); 1308 | } 1309 | 1310 | // Parse class names 1311 | var classNames = []; 1312 | if (viewport.getAttribute('class')) { 1313 | classNames = viewport.getAttribute('class').split(' ') 1314 | } 1315 | 1316 | // Set class (if not set already) 1317 | if (!~classNames.indexOf('svg-pan-zoom_viewport')) { 1318 | classNames.push('svg-pan-zoom_viewport') 1319 | viewport.setAttribute('class', classNames.join(' ')) 1320 | } 1321 | 1322 | return viewport 1323 | } 1324 | 1325 | /** 1326 | * Set SVG attributes 1327 | * 1328 | * @param {SVGSVGElement} svg 1329 | */ 1330 | , setupSvgAttributes: function(svg) { 1331 | // Setting default attributes 1332 | svg.setAttribute('xmlns', this.svgNS); 1333 | svg.setAttributeNS(this.xmlnsNS, 'xmlns:xlink', this.xlinkNS); 1334 | svg.setAttributeNS(this.xmlnsNS, 'xmlns:ev', this.evNS); 1335 | 1336 | // Needed for Internet Explorer, otherwise the viewport overflows 1337 | if (svg.parentNode !== null) { 1338 | var style = svg.getAttribute('style') || ''; 1339 | if (style.toLowerCase().indexOf('overflow') === -1) { 1340 | svg.setAttribute('style', 'overflow: hidden; ' + style); 1341 | } 1342 | } 1343 | } 1344 | 1345 | /** 1346 | * How long Internet Explorer takes to finish updating its display (ms). 1347 | */ 1348 | , internetExplorerRedisplayInterval: 300 1349 | 1350 | /** 1351 | * Forces the browser to redisplay all SVG elements that rely on an 1352 | * element defined in a 'defs' section. It works globally, for every 1353 | * available defs element on the page. 1354 | * The throttling is intentionally global. 1355 | * 1356 | * This is only needed for IE. It is as a hack to make markers (and 'use' elements?) 1357 | * visible after pan/zoom when there are multiple SVGs on the page. 1358 | * See bug report: https://connect.microsoft.com/IE/feedback/details/781964/ 1359 | * also see svg-pan-zoom issue: https://github.com/ariutta/svg-pan-zoom/issues/62 1360 | */ 1361 | , refreshDefsGlobal: Utils.throttle(function() { 1362 | var allDefs = document.querySelectorAll('defs'); 1363 | var allDefsCount = allDefs.length; 1364 | for (var i = 0; i < allDefsCount; i++) { 1365 | var thisDefs = allDefs[i]; 1366 | thisDefs.parentNode.insertBefore(thisDefs, thisDefs); 1367 | } 1368 | }, this.internetExplorerRedisplayInterval) 1369 | 1370 | /** 1371 | * Sets the current transform matrix of an element 1372 | * 1373 | * @param {SVGElement} element 1374 | * @param {SVGMatrix} matrix CTM 1375 | * @param {SVGElement} defs 1376 | */ 1377 | , setCTM: function(element, matrix, defs) { 1378 | var that = this 1379 | , s = 'matrix(' + matrix.a + ',' + matrix.b + ',' + matrix.c + ',' + matrix.d + ',' + matrix.e + ',' + matrix.f + ')'; 1380 | 1381 | element.setAttributeNS(null, 'transform', s); 1382 | 1383 | // IE has a bug that makes markers disappear on zoom (when the matrix "a" and/or "d" elements change) 1384 | // see http://stackoverflow.com/questions/17654578/svg-marker-does-not-work-in-ie9-10 1385 | // and http://srndolha.wordpress.com/2013/11/25/svg-line-markers-may-disappear-in-internet-explorer-11/ 1386 | if (_browser === 'ie' && !!defs) { 1387 | // this refresh is intended for redisplaying the SVG during zooming 1388 | defs.parentNode.insertBefore(defs, defs); 1389 | // this refresh is intended for redisplaying the other SVGs on a page when panning a given SVG 1390 | // it is also needed for the given SVG itself, on zoomEnd, if the SVG contains any markers that 1391 | // are located under any other element(s). 1392 | window.setTimeout(function() { 1393 | that.refreshDefsGlobal(); 1394 | }, that.internetExplorerRedisplayInterval); 1395 | } 1396 | } 1397 | 1398 | /** 1399 | * Instantiate an SVGPoint object with given event coordinates 1400 | * 1401 | * @param {Event} evt 1402 | * @param {SVGSVGElement} svg 1403 | * @return {SVGPoint} point 1404 | */ 1405 | , getEventPoint: function(evt, svg) { 1406 | var point = svg.createSVGPoint() 1407 | 1408 | Utils.mouseAndTouchNormalize(evt, svg) 1409 | 1410 | point.x = evt.clientX 1411 | point.y = evt.clientY 1412 | 1413 | return point 1414 | } 1415 | 1416 | /** 1417 | * Get SVG center point 1418 | * 1419 | * @param {SVGSVGElement} svg 1420 | * @return {SVGPoint} 1421 | */ 1422 | , getSvgCenterPoint: function(svg, width, height) { 1423 | return this.createSVGPoint(svg, width / 2, height / 2) 1424 | } 1425 | 1426 | /** 1427 | * Create a SVGPoint with given x and y 1428 | * 1429 | * @param {SVGSVGElement} svg 1430 | * @param {Number} x 1431 | * @param {Number} y 1432 | * @return {SVGPoint} 1433 | */ 1434 | , createSVGPoint: function(svg, x, y) { 1435 | var point = svg.createSVGPoint() 1436 | point.x = x 1437 | point.y = y 1438 | 1439 | return point 1440 | } 1441 | } 1442 | 1443 | },{"./utilities":7}],6:[function(require,module,exports){ 1444 | // uniwheel 0.1.2 (customized) 1445 | // A unified cross browser mouse wheel event handler 1446 | // https://github.com/teemualap/uniwheel 1447 | 1448 | module.exports = (function(){ 1449 | 1450 | //Full details: https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel 1451 | 1452 | var prefix = "", _addEventListener, _removeEventListener, onwheel, support, fns = []; 1453 | 1454 | // detect event model 1455 | if ( window.addEventListener ) { 1456 | _addEventListener = "addEventListener"; 1457 | _removeEventListener = "removeEventListener"; 1458 | } else { 1459 | _addEventListener = "attachEvent"; 1460 | _removeEventListener = "detachEvent"; 1461 | prefix = "on"; 1462 | } 1463 | 1464 | // detect available wheel event 1465 | support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel" 1466 | document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel" 1467 | "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox 1468 | 1469 | 1470 | function createCallback(element,callback,capture) { 1471 | 1472 | var fn = function(originalEvent) { 1473 | 1474 | !originalEvent && ( originalEvent = window.event ); 1475 | 1476 | // create a normalized event object 1477 | var event = { 1478 | // keep a ref to the original event object 1479 | originalEvent: originalEvent, 1480 | target: originalEvent.target || originalEvent.srcElement, 1481 | type: "wheel", 1482 | deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1, 1483 | deltaX: 0, 1484 | delatZ: 0, 1485 | preventDefault: function() { 1486 | originalEvent.preventDefault ? 1487 | originalEvent.preventDefault() : 1488 | originalEvent.returnValue = false; 1489 | } 1490 | }; 1491 | 1492 | // calculate deltaY (and deltaX) according to the event 1493 | if ( support == "mousewheel" ) { 1494 | event.deltaY = - 1/40 * originalEvent.wheelDelta; 1495 | // Webkit also support wheelDeltaX 1496 | originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX ); 1497 | } else { 1498 | event.deltaY = originalEvent.detail; 1499 | } 1500 | 1501 | // it's time to fire the callback 1502 | return callback( event ); 1503 | 1504 | }; 1505 | 1506 | fns.push({ 1507 | element: element, 1508 | fn: fn, 1509 | capture: capture 1510 | }); 1511 | 1512 | return fn; 1513 | } 1514 | 1515 | function getCallback(element,capture) { 1516 | for (var i = 0; i < fns.length; i++) { 1517 | if (fns[i].element === element && fns[i].capture === capture) { 1518 | return fns[i].fn; 1519 | } 1520 | } 1521 | return function(){}; 1522 | } 1523 | 1524 | function removeCallback(element,capture) { 1525 | for (var i = 0; i < fns.length; i++) { 1526 | if (fns[i].element === element && fns[i].capture === capture) { 1527 | return fns.splice(i,1); 1528 | } 1529 | } 1530 | } 1531 | 1532 | function _addWheelListener( elem, eventName, callback, useCapture ) { 1533 | 1534 | var cb; 1535 | 1536 | if (support === "wheel") { 1537 | cb = callback; 1538 | } else { 1539 | cb = createCallback(elem,callback,useCapture); 1540 | } 1541 | 1542 | elem[ _addEventListener ]( prefix + eventName, cb, useCapture || false ); 1543 | 1544 | } 1545 | 1546 | function _removeWheelListener( elem, eventName, callback, useCapture ) { 1547 | 1548 | if (support === "wheel") { 1549 | cb = callback; 1550 | } else { 1551 | cb = getCallback(elem,useCapture); 1552 | } 1553 | 1554 | elem[ _removeEventListener ]( prefix + eventName, cb, useCapture || false ); 1555 | 1556 | removeCallback(elem,useCapture); 1557 | 1558 | } 1559 | 1560 | function addWheelListener( elem, callback, useCapture ) { 1561 | _addWheelListener( elem, support, callback, useCapture ); 1562 | 1563 | // handle MozMousePixelScroll in older Firefox 1564 | if( support == "DOMMouseScroll" ) { 1565 | _addWheelListener( elem, "MozMousePixelScroll", callback, useCapture); 1566 | } 1567 | } 1568 | 1569 | function removeWheelListener(elem,callback,useCapture){ 1570 | _removeWheelListener(elem,support,callback,useCapture); 1571 | 1572 | // handle MozMousePixelScroll in older Firefox 1573 | if( support == "DOMMouseScroll" ) { 1574 | _removeWheelListener(elem, "MozMousePixelScroll", callback, useCapture); 1575 | } 1576 | } 1577 | 1578 | return { 1579 | on: addWheelListener, 1580 | off: removeWheelListener 1581 | }; 1582 | 1583 | })(); 1584 | 1585 | },{}],7:[function(require,module,exports){ 1586 | module.exports = { 1587 | /** 1588 | * Extends an object 1589 | * 1590 | * @param {Object} target object to extend 1591 | * @param {Object} source object to take properties from 1592 | * @return {Object} extended object 1593 | */ 1594 | extend: function(target, source) { 1595 | target = target || {}; 1596 | for (var prop in source) { 1597 | // Go recursively 1598 | if (this.isObject(source[prop])) { 1599 | target[prop] = this.extend(target[prop], source[prop]) 1600 | } else { 1601 | target[prop] = source[prop] 1602 | } 1603 | } 1604 | return target; 1605 | } 1606 | 1607 | /** 1608 | * Checks if an object is a DOM element 1609 | * 1610 | * @param {Object} o HTML element or String 1611 | * @return {Boolean} returns true if object is a DOM element 1612 | */ 1613 | , isElement: function(o){ 1614 | return ( 1615 | o instanceof HTMLElement || o instanceof SVGElement || o instanceof SVGSVGElement || //DOM2 1616 | (o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string') 1617 | ); 1618 | } 1619 | 1620 | /** 1621 | * Checks if an object is an Object 1622 | * 1623 | * @param {Object} o Object 1624 | * @return {Boolean} returns true if object is an Object 1625 | */ 1626 | , isObject: function(o){ 1627 | return Object.prototype.toString.call(o) === '[object Object]'; 1628 | } 1629 | 1630 | /** 1631 | * Checks if variable is Number 1632 | * 1633 | * @param {Integer|Float} n 1634 | * @return {Boolean} returns true if variable is Number 1635 | */ 1636 | , isNumber: function(n) { 1637 | return !isNaN(parseFloat(n)) && isFinite(n); 1638 | } 1639 | 1640 | /** 1641 | * Search for an SVG element 1642 | * 1643 | * @param {Object|String} elementOrSelector DOM Element or selector String 1644 | * @return {Object|Null} SVG or null 1645 | */ 1646 | , getSvg: function(elementOrSelector) { 1647 | var element 1648 | , svg; 1649 | 1650 | if (!this.isElement(elementOrSelector)) { 1651 | // If selector provided 1652 | if (typeof elementOrSelector === 'string' || elementOrSelector instanceof String) { 1653 | // Try to find the element 1654 | element = document.querySelector(elementOrSelector) 1655 | 1656 | if (!element) { 1657 | throw new Error('Provided selector did not find any elements. Selector: ' + elementOrSelector) 1658 | return null 1659 | } 1660 | } else { 1661 | throw new Error('Provided selector is not an HTML object nor String') 1662 | return null 1663 | } 1664 | } else { 1665 | element = elementOrSelector 1666 | } 1667 | 1668 | if (element.tagName.toLowerCase() === 'svg') { 1669 | svg = element; 1670 | } else { 1671 | if (element.tagName.toLowerCase() === 'object') { 1672 | svg = element.contentDocument.documentElement; 1673 | } else { 1674 | if (element.tagName.toLowerCase() === 'embed') { 1675 | svg = element.getSVGDocument().documentElement; 1676 | } else { 1677 | if (element.tagName.toLowerCase() === 'img') { 1678 | throw new Error('Cannot script an SVG in an "img" element. Please use an "object" element or an in-line SVG.'); 1679 | } else { 1680 | throw new Error('Cannot get SVG.'); 1681 | } 1682 | return null 1683 | } 1684 | } 1685 | } 1686 | 1687 | return svg 1688 | } 1689 | 1690 | /** 1691 | * Attach a given context to a function 1692 | * @param {Function} fn Function 1693 | * @param {Object} context Context 1694 | * @return {Function} Function with certain context 1695 | */ 1696 | , proxy: function(fn, context) { 1697 | return function() { 1698 | return fn.apply(context, arguments) 1699 | } 1700 | } 1701 | 1702 | /** 1703 | * Returns object type 1704 | * Uses toString that returns [object SVGPoint] 1705 | * And than parses object type from string 1706 | * 1707 | * @param {Object} o Any object 1708 | * @return {String} Object type 1709 | */ 1710 | , getType: function(o) { 1711 | return Object.prototype.toString.apply(o).replace(/^\[object\s/, '').replace(/\]$/, '') 1712 | } 1713 | 1714 | /** 1715 | * If it is a touch event than add clientX and clientY to event object 1716 | * 1717 | * @param {Event} evt 1718 | * @param {SVGSVGElement} svg 1719 | */ 1720 | , mouseAndTouchNormalize: function(evt, svg) { 1721 | // If no cilentX and but touch objects are available 1722 | if (evt.clientX === void 0 || evt.clientX === null) { 1723 | // Fallback 1724 | evt.clientX = 0 1725 | evt.clientY = 0 1726 | 1727 | // If it is a touch event 1728 | if (evt.changedTouches !== void 0 && evt.changedTouches.length) { 1729 | // If touch event has changedTouches 1730 | if (evt.changedTouches[0].clientX !== void 0) { 1731 | evt.clientX = evt.changedTouches[0].clientX 1732 | evt.clientY = evt.changedTouches[0].clientY 1733 | } 1734 | // If changedTouches has pageX attribute 1735 | else if (evt.changedTouches[0].pageX !== void 0) { 1736 | var rect = svg.getBoundingClientRect(); 1737 | 1738 | evt.clientX = evt.changedTouches[0].pageX - rect.left 1739 | evt.clientY = evt.changedTouches[0].pageY - rect.top 1740 | } 1741 | // If it is a custom event 1742 | } else if (evt.originalEvent !== void 0) { 1743 | if (evt.originalEvent.clientX !== void 0) { 1744 | evt.clientX = evt.originalEvent.clientX 1745 | evt.clientY = evt.originalEvent.clientY 1746 | } 1747 | } 1748 | } 1749 | } 1750 | 1751 | /** 1752 | * Check if an event is a double click/tap 1753 | * TODO: For touch gestures use a library (hammer.js) that takes in account other events 1754 | * (touchmove and touchend). It should take in account tap duration and traveled distance 1755 | * 1756 | * @param {Event} evt 1757 | * @param {Event} prevEvt Previous Event 1758 | * @return {Boolean} 1759 | */ 1760 | , isDblClick: function(evt, prevEvt) { 1761 | // Double click detected by browser 1762 | if (evt.detail === 2) { 1763 | return true; 1764 | } 1765 | // Try to compare events 1766 | else if (prevEvt !== void 0 && prevEvt !== null) { 1767 | var timeStampDiff = evt.timeStamp - prevEvt.timeStamp // should be lower than 250 ms 1768 | , touchesDistance = Math.sqrt(Math.pow(evt.clientX - prevEvt.clientX, 2) + Math.pow(evt.clientY - prevEvt.clientY, 2)) 1769 | 1770 | return timeStampDiff < 250 && touchesDistance < 10 1771 | } 1772 | 1773 | // Nothing found 1774 | return false; 1775 | } 1776 | 1777 | /** 1778 | * Returns current timestamp as an integer 1779 | * 1780 | * @return {Number} 1781 | */ 1782 | , now: Date.now || function() { 1783 | return new Date().getTime(); 1784 | } 1785 | 1786 | // From underscore. 1787 | // Returns a function, that, when invoked, will only be triggered at most once 1788 | // during a given window of time. Normally, the throttled function will run 1789 | // as much as it can, without ever going more than once per `wait` duration; 1790 | // but if you'd like to disable the execution on the leading edge, pass 1791 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 1792 | // jscs:disable 1793 | // jshint ignore:start 1794 | , throttle: function(func, wait, options) { 1795 | var that = this; 1796 | var context, args, result; 1797 | var timeout = null; 1798 | var previous = 0; 1799 | if (!options) options = {}; 1800 | var later = function() { 1801 | previous = options.leading === false ? 0 : that.now(); 1802 | timeout = null; 1803 | result = func.apply(context, args); 1804 | if (!timeout) context = args = null; 1805 | }; 1806 | return function() { 1807 | var now = that.now(); 1808 | if (!previous && options.leading === false) previous = now; 1809 | var remaining = wait - (now - previous); 1810 | context = this; 1811 | args = arguments; 1812 | if (remaining <= 0 || remaining > wait) { 1813 | clearTimeout(timeout); 1814 | timeout = null; 1815 | previous = now; 1816 | result = func.apply(context, args); 1817 | if (!timeout) context = args = null; 1818 | } else if (!timeout && options.trailing !== false) { 1819 | timeout = setTimeout(later, remaining); 1820 | } 1821 | return result; 1822 | }; 1823 | } 1824 | // jshint ignore:end 1825 | // jscs:enable 1826 | 1827 | /** 1828 | * Create a requestAnimationFrame simulation 1829 | * 1830 | * @param {Number|String} refreshRate 1831 | * @return {Function} 1832 | */ 1833 | , createRequestAnimationFrame: function(refreshRate) { 1834 | var timeout = null 1835 | 1836 | // Convert refreshRate to timeout 1837 | if (refreshRate !== 'auto' && refreshRate < 60 && refreshRate > 1) { 1838 | timeout = Math.floor(1000 / refreshRate) 1839 | } 1840 | 1841 | if (timeout === null) { 1842 | return window.requestAnimationFrame || requestTimeout(33) 1843 | } else { 1844 | return requestTimeout(timeout) 1845 | } 1846 | } 1847 | } 1848 | 1849 | /** 1850 | * Create a callback that will execute after a given timeout 1851 | * 1852 | * @param {Function} timeout 1853 | * @return {Function} 1854 | */ 1855 | function requestTimeout(timeout) { 1856 | return function(callback) { 1857 | window.setTimeout(callback, timeout) 1858 | } 1859 | } 1860 | 1861 | },{}]},{},[1]); 1862 | --------------------------------------------------------------------------------