Excel裏有一個數據透視圖功能在作數據探索時很是好用。它容許用戶選擇列名、行名,根據選擇結果繪圖。前端
本文利用Shiny實現一個相似數據透視圖的可視化工具。python
數據分析師常常須要看數據。一般而言,數據或存放在MySQL數據庫,或存放在Hadoop集羣,或存放在阿里雲的ODPS上。分析師根據業務需求寫SQL語句從數據平臺上提取出須要的數據,隨後就面臨着本文要重點討論的怎麼對數據可視化的難題。react
對於一個固定的需求,一般須要觀察多組數據。普通一點的分析師,多是拷貝出一組數據,貼到Excel裏,繪個圖看一下,而後拷貝下一組數據;高級一點的分析師,多是用R寫好一段代碼,而後修改分組的變量取值重複運行代碼來觀察多組數據。我在工做中動輒須要觀察一百組數據,上述兩種方法仍然不夠好用,最好的方法是我鼠標點擊一百次,每點擊一次產生一幅圖。程序員
更可惡的是,每來一個新需求,不管是Excel仍是R都得根據新需求定製化一遍操做或一套代碼。sql
因而某一天,我實在忍不了,就嘗試作了一個工具,將SQL寫完後的數據可視化工做給工程化了。數據庫
這個工具首先支持select查詢語句,執行成功後會顯示執行結果,同時提供一個設置面板,讓用戶選擇數據分組字段、x軸字段、y軸字段,而後生成分組結果,每點擊一個結果,生成該分組數據的圖。目前該工具只支持時間序列數據,可以繪製點圖和線圖。編程
Shiny讓數據分析師寫完分析與可視化代碼後,稍微再花幾十分鐘,就能夠把分析代碼工程化,將分析成果快速轉化爲交互式網頁分享給別人。因此,若是你是一名使用R的數據分析師,選擇Shiny是很是明智的,由於它不須要你有新的技能,且開發起來實在太快。它跟一般咱們瞭解的其餘框架不同:其餘框架通常都是先後端分離,後端提供json,前端根據json繪圖繪表,須要若干個程序員協同開發完成。然而這種可視化的小工具每每是得不到研發資源的支持,只能本數據分析師一人操刀先後端全包。json
當一個項目以數據計算和可視化爲核心,只投入數據分析師一我的,要求快速實現效果,對執行效率和負載無要求,那麼shiny無疑是一個很是誘人的方案。segmentfault
廢話很少說,拷貝如下代碼至RStudio,運行APP便可。有朋友反饋說表格畫不出來,我推測是DT控件沒裝好,重裝一下試試。將代碼中的debug=TRUE改成debug=FALSE,添加本身須要的數據接口就能夠直接使用了。後端
######################### # 時間序列數據可視化工具 # @author: shuiping.chen@alibaba-inc.com # @date: 2016-07-10 ######################### library(shiny) library(shinyjs) library(DT) library(dplyr) library(tidyr) library(stringr) library(ggplot2) library(scales) library(plotly) run.sql <- function(sql, debug=FALSE) { if(debug==FALSE){ df <- XXXXX # 自行定義函數,根據數據存儲位置,執行SQL語句 } else{ # 測試數據 group_id <- rep(1, nrow(economics)) dt <- paste(as.character(economics$date), "00:00:00") df <- cbind(group_id, dt, economics) } return(df) } ui <- fluidPage( useShinyjs(), titlePanel("時間序列數據可視化工具"), # 第一部分:SQL命令提交界面 div(id="download", fluidRow( column(12, textOutput(outputId="download_info") ) ), fluidRow( column(12, HTML( paste('<textarea id="sql_cmd" rows="10", cols="180">', "select * from xxxx limit 1000;", '</textarea>') ) ) ), fluidRow( column(12, actionButton(inputId="refresh_button", label="加載數據", icon=icon("submit") ) ) ) ), shinyjs::hidden( div(id="table", # 第二部分:SQL命令執行結果顯示 hr(), dataTableOutput(outputId="sql_tab"), # 第三部分:可視化規則設置 hr(), textOutput(outputId="tab_button_message"), sidebarLayout( div(id="table_tool", sidebarPanel( selectInput(inputId="group_fields", label="繪圖分組字段", choices=NULL, selected=NULL, multiple=TRUE), selectInput(inputId="x_field", label="設置x軸字段,必須是日期時間", choices=NULL, selected=NULL, multiple=FALSE), selectInput(inputId="y_line_fields", label="設置y軸線圖字段", choices=NULL, selected=NULL, multiple=TRUE), selectInput(inputId="y_point_fields", label="設置y軸點圖字段", choices=NULL, selected=NULL, multiple=TRUE), selectInput(inputId="group_shape_field", label="設置點圖形狀字段", choices=NULL, selected=NULL, multiple=FALSE), actionButton(inputId="tab_button", label="顯示分組表格", icon=icon("submit")), width=3 ) ), div(id="group_content", mainPanel(dataTableOutput(outputId="group_tab"), width=9 ) ) ) ) ), # 第四部分:可視化圖形 shinyjs::hidden( div(id = "plot", hr(), plotlyOutput(outputId="case_viewer", height="600px") ) ) ) server <- function(input, output, session) { observe({ # 檢查SQL輸入框 if(is.null(input$sql_cmd) | input$sql_cmd == "") { shinyjs::disable("refresh_button") } else{ shinyjs::enable("refresh_button") } # 檢查可視化規則設置 if (input$x_field == "" | (is.null(input$y_line_fields) & is.null(input$y_point_fields)) | is.null(input$group_fields)) { shinyjs::disable("tab_button") } else { shinyjs::enable("tab_button") } }) # 執行SQL命令獲取數據 sql_data <- eventReactive(input$refresh_button, { cat(file=stderr(), "#### event log ####: refresh button clicked\n") shinyjs::disable("refresh_button") shinyjs::hide(id = "table", anim = TRUE) shinyjs::hide(id = "plot", anim = TRUE) res <- run.sql(input$sql_cmd, debug=TRUE) updateSelectInput(session, inputId="group_fields", choices=colnames(res)) updateSelectInput(session, inputId="x_field", choices=colnames(res)) updateSelectInput(session, inputId="y_line_fields", choices=colnames(res)) updateSelectInput(session, inputId="y_point_fields", choices=colnames(res)) updateSelectInput(session, inputId="group_shape_field", choices=c("無",colnames(res)), selected="無") shinyjs::enable("refresh_button") shinyjs::show(id = "table", anim = TRUE) shinyjs::hide(id = "group_content", anim = FALSE) return(res) }) # SQL命令執行狀態 output$download_info <- renderText({ if(input$refresh_button == 0){ message <- "請敲入SQL select查詢語句,點擊按鈕提交" } else{ message <- isolate({paste0("表格下載成功!總行數", nrow(sql_data()), ",總列數", ncol(sql_data()), ",更新時間是", as.character(lubridate::now(), format="%Y-%m-%d %H:%M:%S")) }) } message }) # 顯示SQL執行結果 output$sql_tab <- DT::renderDataTable({ datatable(sql_data(), filter='top', selection='single') }) # 獲取繪圖分組結果 group_data <- eventReactive(input$tab_button, { cat(file=stderr(), "#### event log ####: tab button clicked\n") res <- sql_data() %>% select(one_of(input$group_fields)) %>% distinct() shinyjs::show(id="group_content", anim=TRUE) return(res) }) output$tab_button_message <- renderText({ if(input$tab_button == 0) { message <- "請在下方左側設置數據可視化規則; 點擊按鈕後,下方右側將以表格顯示數據分組結果; 點擊表格的一行,將在下方繪製該行所指分組數據的圖形" } else { message <- isolate({paste0("繪圖分組數", nrow(group_data()), ",更新時間是", as.character(lubridate::now(), format="%Y-%m-%d %H:%M:%S")) }) } message }) # 顯示繪圖分組結果 output$group_tab <- DT::renderDataTable({ datatable(group_data(), filter='top', selection='single') }) # 顯示繪圖 observeEvent(input$group_tab_rows_selected, { cat(file=stderr(), paste0("#### event log ####: group table row ", input$group_tab_rows_selected, " clicked\n")) output$case_viewer <- renderPlotly({ s <- input$group_tab_row_last_clicked cat(file=stderr(), "#### event log ####: table row", s, "clicked\n") p <- ggplot() filter_str <- isolate({str_c(group_data()[s, input$group_fields], collapse="_")}) # 使用_以配合unite方法 target_plot_data <- sql_data() %>% unite_("new_var", input$group_fields, remove=FALSE) %>% filter(new_var==filter_str) if(length(input$y_line_fields) > 0) { target_plot_data$dt <- lubridate::ymd_hms(target_plot_data[,input$x_field], tz="UTC-8") line_df <- target_plot_data %>% tidyr::gather(col_name, thresh, one_of(input$y_line_fields)) %>% dplyr::mutate(thresh=as.numeric(thresh)) p <- p + geom_line(data=line_df, aes(x=dt,y=thresh,color=col_name)) } if(length(input$y_point_fields) > 0) { target_plot_data$dt <- lubridate::ymd_hms(target_plot_data[,input$x_field], tz="UTC-8") point_df <- target_plot_data %>% tidyr::gather(col_name, thresh, one_of(input$y_point_fields)) %>% dplyr::mutate(thresh=as.numeric(thresh)) if(input$group_shape_field != "無") { point_df[, input$group_shape_field] <- as.factor(point_df[, input$group_shape_field]) p <- p + geom_point(data=point_df, aes_string(x="dt",y="thresh",color="col_name", shape=input$group_shape_field)) } else{ p <- p + geom_point(data=point_df, aes(x=dt,y=thresh,color=col_name)) } } p <- p ggplotly(p) }) shinyjs::show("plot", anim = TRUE) }) } shinyApp(ui=ui, server=server)
注:爲了讓用戶明白工具的使用方法,代碼採用shinyjs在適當的時機隱藏/顯示對應的組件;在eventReactive事件驅動的計算中,須要保證至少一個依賴與該reactive的組件處於顯示狀態,不然沒法觸發計算,observeEvent不存在此問題。
關於做者:丹追兵:數據分析師一枚,編程語言python和R,使用Spark、Hadoop、Storm、ODPS。本文出自丹追兵的pytrafficR專欄,轉載請註明做者與出處:https://segmentfault.com/blog...