🐧

shiny.fluent その1

2021/11/13に公開

はじめに

shiny.fluent は、Microsoft Fluent UI を Shiny で使うためのパッケージです。
https://github.com/Appsilon/shiny.fluent

Fluent UI はFluent Design Systemを実装したものです。これは次の記事にあるように四つの原則にもとづいているとのことです。

https://medium.com/microsoft-design/four-principles-for-the-future-of-design-78922340cece

Shiny Fluent Tutorial: Build Beautiful Shiny Apps をみるとshiny.fluentの使い始め方がわかります。

https://www.youtube.com/watch?v=KKWAeeZM4Rc&t=2621s

内容は、概ね、Tutorial: Create your first shiny.fluent dashboardと同じでした。

以下、なぞってみます。

インストール

githubから直接インストールします。

remotes::install_github("Appsilon/shiny.react")
remotes::install_github("Appsilon/shiny.fluent")

リソース

https://demo.appsilon.com/apps/fluentui/#!/

https://developer.microsoft.com/en-us/fluentui#/controls/web

Hello World アプリ

上記チュートリアルでは、初めにHello Worldアプリをつくり、ShinyアプリとしてFluent UIの機能とコンポーネントを使ってテキストを表示する方法を提示しています。

app.R
library(shiny)
library(shiny.fluent)

ui <- fluentPage(
  Text("Hello World !", variant = "mega")
)

server <- function(input, output) {}

shinyApp(ui = ui, server = server)

UIは、shiny.fluent のfluentPage() を使っています。

テキストは、shiny.fluent のText()コンポーネントをつかって表示しています。ここでは、variant引数で文字のサイズを指定しています。このようなTextコンポーネントのオプションは、Microsoft Fluent UI ReactのTextに説明があります。

データを表で表示する

データテーブルを表で表示します。データは、shiny.flunent付属のダミーデータ fluentSalesDealsを使用してます。

app.R
library(shiny)
library(shiny.fluent)
library(tibble)

columns <- tibble(
  fieldName = c('rep_name', "date", "deal_amount", "city", "is_closed"),
  name = c("Sales rep", "Close date", "Amount", "City", "Is closed?")
)

ui <- fluentPage(
  Text("Hello !", variant = "mega"),
  uiOutput("table")
)

server <- function(input, output) {
  output$table <- renderUI({
    DetailsList(items = fluentSalesDeals,
                columns = columns)
  })
}

shinyApp(ui = ui, server = server)

UIに、uiOutput()をつかって、表のアウトプットとしています。対応したアウトプットをserverにて renderUI()を使って作成しています。このように、専用のアウトプットやレンダーを提供せず、UI系のものを使っているようです。

shiny.fluentのDetailsList()コンポーネントを使ってデータを表として表示しています。表に表示するカラムと表示名の指定を、columns引数でおこなっています。カラム名の指定は、データフレームの形でデータ上のカラム名 fieldName"rep_name" と表示上のカラム名 name"Sales rep" を指定しています。

オプションはshiny.fluentのDetailsListMicrosoft Fluent UI ReactのDetailsListに詳しいです。が、オプションについてはまだ正直よくわかりませんでした。

簡単な入力を追加する

簡単な入力例として、表データをフィルタするトグルを追加しています。表の is_closed の値が0の場合は、まだオープンな取引ということなので、これをフィルタするトグルを入力として追加しています。データは入力に従ってフィルタされるため、reactive() を使っています。

app.R
library(shiny)
library(shiny.fluent)
library(tibble)
library(dplyr)

columns <- tibble(
  fieldName = c('rep_name', "date", "deal_amount", "city", "is_closed"),
  name = c("Sales rep", "Close date", "Amount", "City", "Is closed?")
)

ui <- fluentPage(
  Toggle.shinyInput("includeOpen", label = "Include open deals"),
  Text("Hello !", variant = "mega"),
  uiOutput("table")
)

server <- function(input, output) {
  filteredData <- reactive({
    fluentSalesDeals %>%
      filter(
        is_closed | input$includeOpen
      )
  })

  output$table <- renderUI({
    DetailsList(items = filteredData,
                columns = columns
  })
}

shinyApp(ui = ui, server = server)

shiny.fluentのToggle.shinyInputをつかって、トグルスイッチの入力を作成しています。第一引数がインプットの名前、ラベル引数が使えます。コンポーネント名に.shinyInputとついている関数が提供されていました。

トグルの値はゼロイチなので、フィルタにこれをつかっています。フィルタ対象カラム is_closed の値もゼロイチなので、これらの論理和(OR)を用いると、トグルの値がゼロの時にis_closedの値が1のもののみをフィルタアウトすること、トグルの値が1のときにis_closedの値がゼロでも1でもフィルタアウト(フィルタされない)することができます。

オプションは、Microsoft Fluent UI ReactのToggleに詳しいです。たとえば、トグルスイッチの脇にOn/Offを明示する場合には、次のようにすることがわかります。

Toggle.shinyInput("includeOpen", label = "Include open deals", 
		  onText = "On", offText = "Off")	

Cardをつかって見た目を良くする

ページ上のパーツを陰影や間隔をつけて見た目で区別しやすいようにします。ここでは、カードのメタファーで、見た目の区別しやすさを考えています。Fluent UI Style Elevationdivのクラスをつかった見た目の制御があります。ここでは、ms-depth-8 を使います。

ui <- fluentPage(
  div(class = "ms-depth-8",
    Toggle.shinyInput("includeOpen", label = "Include open deals"),
  ),
  div(class = "ms-depth-8",
    Text("Hello !", variant = "mega"),
    uiOutput("table")
  )
)

つぎに、paddingなどを設定して、さらに見やすくします。

ui <- fluentPage(
  Stack(
    class = "ms-depth-8",    
    tokens = list(padding = 20, childrenGap = 10),  
   Text("Filter", variant = "large),
    Toggle.shinyInput("includeOpen", label = "Include open deals"),
  ),
  div(
    class = "ms-depth-8",    
    tokens = list(padding = 20, childrenGap = 10),
    Text("Sales deals details", variant = "large"),
    uiOutput("table")
  )
)

shiny.fluentのStackを使用して、コンポーネントを整列しています。Microsoft Fluent UI ReactのStackpaddingchildrenGapの説明があります。

これらのdivでは、classを指定し、paddingを指定し、childrenGapを指定し、タイトルのテキストがあり、コンポーネントがあります。これらの見た目の指定を使いまわせるように関数化します。

Card <- function(..., title = NULL) {
  Stack(
    class = "ms-depth-8",
    tokens = list(padding = 20, childrenGap = 10),
    if (!is.null(title)) Text(title, variant = "large"),
    ...
  )
}

このように関数化すると、次のように使用できます。

filter <- tagList(
  Toggle.shinyInput("includeOpen", label = "Include open deals")
)

Card(title = "Filter", filter),
Card(title = "Hello.", uiOutput("table"))

Cardはタイトルが指定でき、内容があるものとして関数化できました。filter用のCardでは、tagListを使って、Cardの中のコンポーネントを列挙しています。

Gridレイアウト

レイアウトをグリッドにして、デバイスに対してフレキシブルな配置を実現しています。Microsoft Fluent UI ReactのLayoutにGridレイアウトの説明があり、定義に使用できるクラス名とその説明があります。

クラス名 ms-Grid が Grid レイアウトの宣言となり、ms-Grid-col はカラムであることを指定し、ms-sm12 はウィンドウサイズがsmall(320px - 479px)のときに幅が12カラムとなるように指定しています。

div(class="ms-Grid", dir="ltr", style = "padding: 0px",
  div(class="ms-Grid-col ms-sm12", style = "passing: 10px", 
    Card(title = "Filter", filter),
  ),
  div(class="ms-Grid-col ms-sm12", style = "passing: 10px", 
    Card(title = "Hello.", uiOutput("table"))
  )
)

繰り返されるdivの定義部分、一個めのdivとそれ以降のdivをそれぞれGridGridItemとして二つ関数化すると、つぎのようにできるようになります。

Grid <- function(...) {
  div(
    class = "ms-Grid",
    dir = "ltr",
    style = "padding: 0px",
    ...
  )
}

GridItem <- function(..., class = "ms-sm12") {
  div(
    class = paste("ms-Grid-col", class),
    style = "padding: 10px",
    ...
  )
}

Grid(
  GridItem(Card(title = "Filter", filter)),
  GridItem(Card(title = "Hello.", uiOutput("table"))
)

Grid には、GridItem を引数としてとることができ、GridItem には、Card を引数としてとることができました。

他のフィルターを追加する

fluentSalesDealsデータのSales repのフィルターを追加します。fluentPeopleデータを使います。このデータは、key, imageUrl, imageInitials, text, secandaryText, tertiaryText, optionalText, isValid, presence, canExpand, colorのカラムで構成されています。

NormalPeoplePicker.shinyInputは、PeoplePickerをShinyで使用できるようにしたもので、データに含まれる名前や画像情報、プレゼンス情報からなるコンポーネントを人物選択フォームの選択時に適用できるようにし、リッチな人物選択フォームを実現しています。

filters <- tagList(
  div(
    Label("Salse Representative"),
    NormalPeoplePicker.shinyInput("people", options = fluentPeople)
  ),
  Toggle.shinyInput("includeOpen", label = "Include open deals")
)

NormalPeoplePicker の選択の値は input$people として データのフィルタに使用します。

  filteredData <- reactive({
    fluentSalesDeals %>% filter(
      length(input$people) == 0 | rep_id %in% input$people,
      is_closed | input$includeOpen
    )
  })

ここでは filter で論理和(OR)をつかい、input$people で選択数がゼロの時はすべてTRUEとなるので全レコードがのこり、input$people に選択数が1個以上ある場合はOR以降の条件でフィルタされるようにしています。つまり無選択では全レコードを表示するためにフィルタされず、誰かが選択されているその条件でフィルタされるというものです。

棒グラフを追加する

いくつかのフィルター入力でデータを選択できるようになったので、選択したデータを棒グラフで表示するようにしています。

ここでは、ウィンドウの幅に対応してフィルターと棒グラフの配置を変えるようにGridレイアウトのクラスを指定しています。フィルターはms-sm12ms-xl4を指定し、棒グラフはms-sm12ms-xl8と指定します。こうすると、ウインドウ幅が狭い(small, 320px - 479px)ときはどちらも幅12カラムとなり、広い(extra large, 1024px - 1365px)ときはフィルターが幅4カラム、棒グラフが幅8カラムとなるようになります。Layoutに解説があります。

棒グラフの表示には、plotlyOutputを使用し、高さを指定しています。

ui <- fluentPage(
  Grid(
    GridItem(class = "ms-sm12 ms-xl4",
             Card(title = "Filters", filters)
    ),
    GridItem(class = "ms-sm12 ms-xl8",
             Card(title = " Deals count",
                  div(
                    plotlyOutput("plot", height = "300px")
                  )
              )
    ),
    GridItem(
      Card(title = "Deals data",
           div(style = 'height: 500px; overflow: auto',
               uiOutput("table")
           )
      )
    )
  )
)

棒グラフの描画には、ggplot2とPlotlyを使用しています。

  output$plot <- renderPlotly({
    ggplot(filteredData(), aes(x = rep_name)) +
      geom_bar(fill = unique(filteredData()$color)) +
      xlab("Sales rep") +
      ylab("Number of deals") +
      theme_light()
  })

おわりに

チュートリアルをひととおり見た時のメモとして作成しました。PeoplePickerのようなリッチなUIやGridレイアウトがすぐに使えることがわかったのはよかったです。

一方で、shiny.fluent の使い始めについてはわかったのですが、DetailsListでカラム名をクリックするとソートできるようするにはどうすれば良いのかなどの実際の使用するときの要求事項の実装方法についてはまだわからない状態です。Reactの理解がある程度ないと使いこなせないのかと思いました。

次は、Tutorial: Build a Full Shiny Dashboard With shiny.fluentを見てみます。

Discussion