├── .gitignore ├── html_button.html ├── toggle_script.js ├── warehouse_location.Rproj ├── google-analytics.js ├── gomap.js ├── global.R ├── README.md ├── styles.css ├── server.R └── ui.R /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | api_key 6 | data/* 7 | -------------------------------------------------------------------------------- /html_button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /toggle_script.js: -------------------------------------------------------------------------------- 1 | function myFunction() { 2 | var x = document.getElementById("controls"); 3 | if (x.style.display === "none") { 4 | x.style.display = "block"; 5 | } else { 6 | x.style.display = "none"; 7 | } 8 | } -------------------------------------------------------------------------------- /warehouse_location.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | -------------------------------------------------------------------------------- /google-analytics.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date(); 3 | a=s.createElement(o), m=s.getElementsByTagName(o)[0]; 4 | a.async=1; 5 | a.src=g;m.parentNode.insertBefore(a,m); 6 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 7 | 8 | ga('create', 'UA-129389361-1', 'auto'); 9 | ga('send', 'pageview'); -------------------------------------------------------------------------------- /gomap.js: -------------------------------------------------------------------------------- 1 | // When locator icon in datatable is clicked, go to that spot on the map 2 | $(document).on("click", ".go-map", function(e) { 3 | e.preventDefault(); 4 | $el = $(this); 5 | var lat = $el.data("lat"); 6 | var long = $el.data("long"); 7 | var zip = $el.data("zip"); 8 | $($("#nav a")[0]).tab("show"); 9 | Shiny.onInputChange("goto", { 10 | lat: lat, 11 | lng: long, 12 | zip: zip, 13 | nonce: Math.random() 14 | }); 15 | }); -------------------------------------------------------------------------------- /global.R: -------------------------------------------------------------------------------- 1 | library(dplyr) 2 | require(curl) 3 | 4 | get_place_name<-function(lng,lat,api_key){ 5 | base_url<-"https://maps.googleapis.com/maps/api/geocode/json?" 6 | latlng<-paste0("latlng=",lat,",",lng) 7 | sensor<-paste0("sensor=false") 8 | key<-paste0("key=",api_key) 9 | result_type<-paste0("result_type=administrative_area_level_2") 10 | 11 | urls<-paste(paste0(base_url,latlng),sensor,key,result_type,sep="&") 12 | res<-NULL 13 | for(url in urls){ 14 | result<-jsonlite::fromJSON(curl(url)) 15 | res<-c(res,if_else(is.null(result$results$formatted_address),"Loc Not found",result$results$formatted_address)) 16 | } 17 | 18 | return(res) 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # warehouse_loc_determination 2 | Shiny App for Warehouse location Determination 3 | 4 | This shiny app try to find the best warehousing location based on the demand pattern. For determining the suitable warehousing location 5 | it try to keep the warehouse near the demand to optimize the service levels and outbound logistics cost. 6 | 7 | You can visit http://yklearn.com/shiny/wh_loc_finder to see the demo of this app. 8 | 9 | Structure of the file for loading in the application. 10 | To simulate your scenario you can upload your demand pattern in the app. Don't worry we will not store the file you have uploaded. It exists only till your session is live. 11 | 12 | You can download the template by clicking on download template button. 13 | 14 | When you upload the demand using the csv file. It will be plotted on the map. Here you can see and analyze the geograohical pattern of your demand. 15 | 16 | Select the desired no of warehouse via using the slider. 17 | Next thing to do is to cluster these demand via `K-means` clustering there you may need to wait a while to finish the clustering process. 18 | 19 | ``` 20 | Note: Kmeans clustering algorithm has random initialization method to control this you can change the seed value. 21 | ``` 22 | Once clusterin is done you can find out the suitable warehousing locations by clicking on `Run COG` button. 23 | 24 | 25 | To contribute and modify this app clone the repository by typing 26 | 27 | ``` 28 | git clone https://github.com/yogihbti/warehouse_loc_determination.git 29 | ``` 30 | To get the warehouse location names you need to add the google api_key in the file named api_key. This app will read the file to access the google API. 31 | 32 | 33 | FAQ 34 | 1. How to get the google API Key? 35 | 2. How do i clone this repository? 36 | 3. R packages needed to be installed. 37 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | input[type="number"] { 2 | max-width: 80%; 3 | } 4 | 5 | .outer { 6 | position: fixed; 7 | top: 41px; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | overflow: hidden; 12 | padding: 0; 13 | } 14 | 15 | /* Customize fonts */ 16 | body, label, input, button, select { 17 | font-family: 'Helvetica Neue', Helvetica; 18 | font-weight: 200; 19 | } 20 | h1, h2, h3, h4 { font-weight: 400; } 21 | #toggle_bt { 22 | /* Appearance */ 23 | background-color: transparent; 24 | padding: 0 20px 20px 20px; 25 | cursor: move; 26 | /* Fade out while not hovering */ 27 | border-color: transparent; 28 | 29 | } 30 | 31 | 32 | #controls { 33 | /* Appearance */ 34 | background-color: white; 35 | padding: 0 20px 20px 20px; 36 | cursor: move; 37 | /* Fade out while not hovering */ 38 | opacity: 0.65; 39 | zoom: 0.9; 40 | transition: opacity 500ms 1s; 41 | } 42 | 43 | #controls:hover { 44 | /* Fade in while hovering */ 45 | opacity: 0.95; 46 | transition-delay: 0; 47 | } 48 | 49 | /* Position and style citation */ 50 | #cite { 51 | position: absolute; 52 | bottom: 10px; 53 | left: 10px; 54 | font-size: 12px; 55 | } 56 | 57 | /* If not using map tiles, show a white background */ 58 | .leaflet-container { 59 | background-color: white !important; 60 | } 61 | 62 | /* The switch - the box around the slider */ 63 | .switch { 64 | position: relative; 65 | display: inline-block; 66 | width: 60px; 67 | height: 34px; 68 | } 69 | 70 | /* Hide default HTML checkbox */ 71 | .switch input { 72 | opacity: 0; 73 | width: 0; 74 | height: 0; 75 | } 76 | 77 | /* The slider */ 78 | .slider { 79 | position: absolute; 80 | cursor: pointer; 81 | top: 0; 82 | left: 0; 83 | right: 0; 84 | bottom: 0; 85 | background-color: #ccc; 86 | -webkit-transition: .4s; 87 | transition: .4s; 88 | } 89 | 90 | .slider:before { 91 | position: absolute; 92 | content: ""; 93 | height: 26px; 94 | width: 26px; 95 | left: 4px; 96 | bottom: 4px; 97 | background-color: white; 98 | -webkit-transition: .4s; 99 | transition: .4s; 100 | } 101 | 102 | input:checked + .slider { 103 | background-color: #2196F3; 104 | } 105 | 106 | input:focus + .slider { 107 | box-shadow: 0 0 1px #2196F3; 108 | } 109 | 110 | input:checked + .slider:before { 111 | -webkit-transform: translateX(26px); 112 | -ms-transform: translateX(26px); 113 | transform: translateX(26px); 114 | } 115 | 116 | /* Rounded sliders */ 117 | .slider.round { 118 | border-radius: 34px; 119 | } 120 | 121 | .slider.round:before { 122 | border-radius: 50%; 123 | } 124 | 125 | .mobileShow {display: none;} 126 | 127 | /* Smartphone Portrait and Landscape */ 128 | @media only screen 129 | and (min-device-width : 320px) 130 | and (max-device-width : 480px){ 131 | .mobileShow {display: inline;} 132 | } -------------------------------------------------------------------------------- /server.R: -------------------------------------------------------------------------------- 1 | library(leaflet) 2 | library(RColorBrewer) 3 | library(scales) 4 | library(lattice) 5 | library(dplyr) 6 | 7 | 8 | function(input, output, session) { 9 | 10 | #Define Reactive values 11 | data_read<-reactiveVal(NULL) 12 | cluster_df<-reactiveVal(NULL) 13 | ## Interactive Map ########################################### 14 | 15 | # Create the map 16 | # Tiles selected for Indian region 17 | output$map <- renderLeaflet({ 18 | leaflet() %>% 19 | addTiles( 20 | urlTemplate = "https://{s}.tiles.mapbox.com/v4/openstreetmap.1b68f018/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiamluYWxmb2ZsaWEiLCJhIjoiY2psejFtZG8wMWhnMjNwcGFqdTNjaGF2MCJ9.ZQVAZAw8Xtg4H2YSuG4PlA", 21 | attribution = '© OpenStreetMap contributors by Mapbox' 22 | ) %>% 23 | setView(lng = 79.85, lat = 21.14, zoom = 4) 24 | }) 25 | 26 | # Download file handler 27 | output$downloadData <- downloadHandler( 28 | filename = "geo_data_input.csv", 29 | content = function(file) { 30 | write.csv(data.frame(Longitude="",Latitude="",SalesVal=0), 31 | file, row.names = FALSE) 32 | } 33 | ) 34 | 35 | #load data from file to a reactive variable 36 | 37 | data_read <- reactive({ 38 | #req(input$file1) ## ?req # require that the input is available 39 | #req(input$sample_or_own) 40 | #browser() 41 | inFile <- input$file1 42 | if(input$sample_or_own=="Yes"){ 43 | df <- read.csv("./data/geo_data_input.csv", header = TRUE) 44 | 45 | }else if(is.null(inFile)){ 46 | 47 | df<-data.frame(Longitude=numeric(),Latitude=numeric(),SalesVal=numeric()) 48 | 49 | } else{ 50 | df <- read.csv(inFile$datapath, header = TRUE) 51 | } 52 | 53 | return(df) 54 | 55 | }) 56 | 57 | #observe for input file and reactive value to be available to update the map 58 | 59 | observeEvent( {input$file1 60 | input$sample_or_own 61 | } 62 | ,{ 63 | leafletProxy("map", data = data_read()) %>% 64 | clearShapes() %>% 65 | clearMarkers() %>% 66 | addCircles(lng=~Longitude, lat=~Latitude, radius=~log(SalesVal)*5000,fillOpacity=.4,weight = 1 ,color="orange") 67 | }, 68 | ignoreNULL = FALSE 69 | 70 | ) 71 | 72 | 73 | 74 | 75 | #observe the clustering button clicked- do the clustering to set color groups 76 | observeEvent(input$run_cluster,{ 77 | set.seed(input$seed) 78 | df<-data_read() 79 | df_matrix<-df[,c("Latitude","Longitude")] 80 | cluster_geo=kmeans(df_matrix,input$no_of_wh) 81 | df$cluster<-as.factor(cluster_geo$cluster) 82 | pal<-colorFactor("viridis",df$cluster) 83 | #browser() 84 | leafletProxy("map", data = df) %>% 85 | clearShapes() %>% 86 | clearMarkers() %>% 87 | addCircles(lng=~Longitude, lat=~Latitude, 88 | radius=~log(SalesVal)*5000, 89 | fillOpacity=.4, 90 | weight = 1, 91 | color=~pal(cluster),group=~cluster) 92 | cluster_df(df) 93 | 94 | }) 95 | 96 | observeEvent(input$find_cog,{ 97 | if(!is.null(input$run_cluster)){ 98 | api_key<-readLines("api_key") 99 | df<-cluster_df() 100 | cog_center<- df %>% 101 | group_by(cluster) %>% 102 | summarise(lon_center=weighted.mean(Longitude,SalesVal), 103 | lat_center=weighted.mean(Latitude,SalesVal)) 104 | place_name<-get_place_name(cog_center$lon_center,cog_center$lat_center,api_key) 105 | cog_center$place_name<-place_name 106 | 107 | pal<-colorFactor("viridis",df$cluster) 108 | #browser() 109 | leafletProxy("map", data = df) %>% 110 | clearShapes() %>% 111 | clearMarkers() %>% 112 | addCircles(lng=~Longitude, lat=~Latitude, 113 | radius=~log(SalesVal)*5000, 114 | fillOpacity=.4, 115 | weight = 1,color=~pal(cluster)) %>% 116 | addMarkers(lng=~lon_center,lat=~lat_center,popup=~place_name,data =cog_center) 117 | }else{ 118 | showNotification("First upload a file.") 119 | } 120 | 121 | }) 122 | 123 | } -------------------------------------------------------------------------------- /ui.R: -------------------------------------------------------------------------------- 1 | library(leaflet) 2 | library(shiny) 3 | 4 | 5 | 6 | navbarPage("Warehouse Determination", id="nav", 7 | 8 | tabPanel("Interactive map", 9 | div(class="outer", 10 | 11 | tags$head( 12 | # Include our custom CSS 13 | includeCSS("styles.css"), 14 | includeScript("google-analytics.js"), 15 | includeScript("gomap.js"), 16 | includeScript("toggle_script.js") 17 | ), 18 | 19 | # If not using custom CSS, set height of leafletOutput to a number instead of percent 20 | leafletOutput("map", width="100%", height="100%"), 21 | 22 | absolutePanel(id = "toggle_bt", class = "panel panel-default mobileShow", fixed = TRUE, 23 | draggable = TRUE, top =100, left = 120, right = "auto", bottom = "auto", 24 | width = 120, height = "auto", 25 | includeHTML('html_button.html') 26 | 27 | ), 28 | 29 | 30 | # Shiny versions prior to 0.11 should use class = "modal" instead. 31 | absolutePanel(id = "controls", class = "panel panel-default", fixed = TRUE, 32 | draggable = TRUE, top = 160, left = 20, right = "auto", bottom = "auto", 33 | width = 330, height = "auto", 34 | h4("Find Best Warehousing Locations"), 35 | radioButtons("sample_or_own","I Want to use sample data.", 36 | choices = c("Yes","No"), 37 | selected = "No", 38 | inline=T), 39 | conditionalPanel("input.sample_or_own == 'No'", 40 | h4("Download "), 41 | downloadButton("downloadData", "Download the File Template"), 42 | fileInput("file1", "Choose CSV File", 43 | accept = c( 44 | "text/csv", 45 | "text/comma-separated-values,text/plain", 46 | ".csv") 47 | )), 48 | 49 | tags$hr(), 50 | sliderInput(inputId = "no_of_wh",label = "No of Warehouse",value=4,min = 1,max=10), 51 | numericInput("seed","Seed for K-Means",100,min=0,max=101), 52 | actionButton("run_cluster","Run Clustering"), 53 | tags$hr(), 54 | actionButton("find_cog","Find COG Locations") 55 | 56 | 57 | ) 58 | 59 | ) 60 | ) 61 | 62 | # tabPanel("Data explorer", 63 | # fluidRow( 64 | # column(3, 65 | # selectInput("states", "States", c("All states"="", structure(state.abb, names=state.name), "Washington, DC"="DC"), multiple=TRUE) 66 | # ), 67 | # column(3, 68 | # conditionalPanel("input.states", 69 | # selectInput("cities", "Cities", c("All cities"=""), multiple=TRUE) 70 | # ) 71 | # ), 72 | # column(3, 73 | # conditionalPanel("input.states", 74 | # selectInput("zipcodes", "Zipcodes", c("All zipcodes"=""), multiple=TRUE) 75 | # ) 76 | # ) 77 | # ), 78 | # fluidRow( 79 | # column(1, 80 | # numericInput("minScore", "Min score", min=0, max=100, value=0) 81 | # ), 82 | # column(1, 83 | # numericInput("maxScore", "Max score", min=0, max=100, value=100) 84 | # ) 85 | # ), 86 | # hr(), 87 | # DT::dataTableOutput("ziptable") 88 | # ), 89 | 90 | ) --------------------------------------------------------------------------------