rintrojsでShinyアプリにチュートリアル機能を追加する

この記事では [rintorjs](https://cran.r-project.org/web/packages/rintrojs/) パッケージを利用してShinyアプリにチュートリアル機能を追加する方法を紹介します。

Webアプリケーションにステップ・バイ・ステップのチュートリアル機能を追加するためのJavaScriptライブラリとして [intro.js](https://introjs.com/) が知られています。[rintorjs](https://cran.r-project.org/web/packages/rintrojs/) はその [intro.js](https://introjs.com/) をShinyアプリで利用するためのパッケージです。

## デモ

[こちら](https://terashim.shinyapps.io/begin-shiny-rintrojs/)にデモアプリを公開しました。

### スクリーンショット

![スクリーンショット1](https://qiita-image-store.s3.amazonaws.com/0/78813/cb5d0881-a9ec-2bbe-f4a5-9e320f6ac2df.png)
![スクリーンショット2](https://qiita-image-store.s3.amazonaws.com/0/78813/26103b2c-d039-3ffe-d7c1-33be2d2cbef4.png)

## 元となるShinyアプリを用意する

それでは作り方を解説していきます。

まずはチュートリアルの無いサンプルアプリを作成します。
ここではRStudioで新規Shinyプロジェクトを作成したときに用意されるおなじみのサンプルコードを利用します。

### app.R

```r
library(shiny)

# アプリケーションのUIを定義
ui <- fluidPage(
  
  # アプリケーションのタイトル
  titlePanel("Old Faithful Geyser Data"),
  
  # サイドバー
  sidebarLayout(
    sidebarPanel(
      # スライダー
      sliderInput("bins",
                  "ビンの数:",
                  min = 1,
                  max = 50,
                  value = 30)
    ),
    
    # メインパネル
    mainPanel(
      plotOutput("distPlot")
    )
  )
)

# サーバー側ロジックを定義
server <- function(input, output) {
  
  output$distPlot <- renderPlot({
    # ui.Rから来た input$bins に基づいてビンを生成
    x    <- faithful[, 2] 
    bins <- seq(min(x), max(x), length.out = input$bins + 1)
    
    # 指定のビン数でヒストグラムを描画
    hist(x, breaks = bins, col = 'darkgray', border = 'white')
  })
}

# アプリケーションを起動する
shinyApp(ui = ui, server = server)
```

（もとのサンプルでは英語になっている部分を適宜日本語に変更しています。）

## チュートリアルを追加する

このサンプルアプリを修正してチュートリアル機能を追加していきます。

まずはマシンに`rintrojs`パッケージをインストールしておきます。

```r
install.packages("rintrojs")
```

次に`app.R`を書き換えます。

### app.R

```r
library(shiny)
library(rintrojs) # パッケージを読み込む

# アプリケーションのUIを定義
ui <- fluidPage(
  
  introjsUI(), # これをUI定義のどこかに含める
  
  # アプリケーションのタイトル
  titlePanel("rintrojsで手軽にチュートリアルを実装"),
  
  # サイドバー
  sidebarLayout(
    sidebarPanel(
      introBox( # チュートリアルのステップ
        sliderInput("bins", "ビンの数:", min = 1, max = 50, value = 30), # このステップでハイライトされる要素
        data.step = 1, # ステップ番号
        data.intro = "このスライダーを動かしてビンの数を変更してください" # このステップのメッセージ
      ),
      
      actionButton("tutorial", "チュートリアルを開始")
    ),
    
    # メインパネル
    mainPanel(
      introBox( # チュートリアルのステップ
        plotOutput("distPlot"), # このステップでハイライトされる要素
        data.step = 2, # ステップ番号
        data.intro = "スライダーの値に合わせてこのヒストグラムが変化します" # このステップのメッセージ
      )
    )
  )
)

# サーバー側ロジックを定義
server <- function(input, output, session) {
  
  output$distPlot <- renderPlot({
    # ui.Rから来た input$bins に基づいてビンを生成
    x    <- faithful[, 2] 
    bins <- seq(min(x), max(x), length.out = input$bins + 1)
    
    # 指定のビン数でヒストグラムを描画
    hist(x, breaks = bins, col = 'darkgray', border = 'white')
  })
  
  observeEvent(
    input$tutorial, # tutorialボタンがクリックされたら
    introjs( # チュートリアルを開始する
      session,
      options = list( # オプション設定
        nextLabel = "次へ", # 次のステップへ進むボタンのラベル
        prevLabel = "戻る", # 前のステップへ戻るボタンのラベル
        skipLabel = "やめる", # チュートリアルをスキップするボタンのラベル
        doneLabel = "完了", # チュートリアルを完了するボタンのラベル
        showBullets = FALSE, # 進捗を表す中点マークを表示する/しない
        showProgress = TRUE # プログレスバーを表示する/しない
      )
    )
  )
}

# アプリケーションを起動する
shinyApp(ui = ui, server = server)

```

これを起動すると[デモアプリ](https://terashim.shinyapps.io/begin-shiny-rintrojs/)と同じものが開始します。『チュートリアルを開始』ボタンをクリックして動作を確認してみましょう。

## ポイント

サンプルコードのポイントを押さえていきます。

### rintrojsパッケージの読み込み

```r
library(rintrojs) # パッケージを読み込む
```

### 関数introjsUI()でintro.jsを有効化

UI定義の中のどこかに `introjsUI()` を入れる必要があります。

```r
# アプリケーションのUIを定義
ui <- fluidPage(
  
  introjsUI(), # これをUI定義のどこかに含める
  
  # ...省略...

)
```

### 関数introBox()でステップを定義

`introBox()` でチュートリアルの各ステップを定義します。このとき、そのステップでハイライトされる要素を囲むようにします。
また、引数 `data.step` でステップ番号を、引数 `data.intro` でメッセージを指定します。

```r
      introBox( # チュートリアルのステップ
        sliderInput("bins", "ビンの数:", min = 1, max = 50, value = 30), # このステップでハイライトされる要素
        data.step = 1, # ステップ番号
        data.intro = "このスライダーを動かしてビンの数を変更してください" # このステップのメッセージ
      ),
```

### チュートリアル開始の制御

サーバー側では、トリガーとなるイベント（この例では『チュートリアルを開始』ボタンクリック）を受けてチュートリアルが開始するよう `observeEvent()` と `introjs()` を組み合わせて使用します。
また、ここで`server()`の引数`session`を`introjs()`に渡すこともポイントです。

```r
# サーバー側ロジックを定義
server <- function(input, output, session) {
  
  # ...省略...
  
  observeEvent(
    input$tutorial, # tutorialボタンがクリックされたら
    introjs( # チュートリアルを開始する
      session,
      options = list( # オプション設定

        # ...省略...

      )
    )
  )
}
```

#### intro.jsオプションの設定

`introjs()`の引数`options`で様々なオプションを設定します。設定可能なオプション項目については[intro.jsの公式ドキュメント](https://introjs.com/docs/intro/options/)を参照ください。
今回のサンプルではボタンのラベルがデフォルトで英語になっているところを日本語にするなどの変更を行いました。

```r
    introjs( # チュートリアルを開始する
      session,
      options = list( # オプション設定
        nextLabel = "次へ", # 次のステップへ進むボタンのラベル
        prevLabel = "戻る", # 前のステップへ戻るボタンのラベル
        skipLabel = "やめる", # チュートリアルをスキップするボタンのラベル
        doneLabel = "完了", # チュートリアルを完了するボタンのラベル
        showBullets = FALSE, # 進捗を表す中点マークを表示する/しない
        showProgress = TRUE # プログレスバーを表示する/しない
      )
    )
```

-----

以上、[rintorjs](https://cran.r-project.org/web/packages/rintrojs/) を利用してShinyアプリにチュートリアル機能を追加する方法を解説しました。
サンプルコードはGitHub ([terashim/begin-shiny-rintrojs](https://github.com/terashim/begin-shiny-rintrojs)) でも公開しています。

[rintorjs](https://cran.r-project.org/web/packages/rintrojs/) には他にも`introjs()`と類似の関数`hintjs()`が用意されています。こちらはステップ・バイ・ステップのチュートリアルではなくUI上の各部にヒントボタンを表示するためのものです。`hintjs()`の利用法については他の機会にご紹介できればと思います。