Rで解析:タブと入力欄の揃ったShinyアプリを少ないコードで作る「EZRShiny」パッケージ
「EZRShiny」パッケージは、ナビゲーションバーとタブを備えたShinyアプリの画面を、少ない行数で見た目の揃った形に組み立てられるパッケージです。入力欄や出力欄のラベルに説明のツールチップを付けたり、操作に合わせてタブやボタンを出し入れしたりもできます。渡すのはタブの名前と、その中に並べる入力欄・出力欄で、手元のデータフレームを見せるための画面をその並びのとおりに書いていきます。shinyとbslibの上に乗る拡張なので、UIとserverに分けるShinyアプリの基本的な書き方を知っていることが前提です。集計結果を確かめる画面を周りの人と共有したい人が、UIの組み立てに時間をかけずに画面を用意できるのではないかと考えます。
パッケージバージョンは0.1.0。Windows 11 x64 (build 26200)のR version 4.6.1で確認しています。
<おすすめのRに関する書籍です>
パッケージのインストール
下記コマンドを実行してください。
# パッケージのインストール
install.packages("EZRShiny")
# パッケージの読み込み
library("EZRShiny")コマンド例
詳細はコメント、パッケージのヘルプを確認してください。例は、京都府内の直売所に九条ねぎ・賀茂なす・万願寺とうがらしが週ごとに出荷された量を記録した架空のデータです。このデータを見せる画面を、部品ごとに作っていきます。
EZRShinyの入力欄・出力欄のコマンドは、Shinyの画面部品(HTML)を返します。単独で実行するとHTMLが表示されるだけなので、実際の見た目はアプリを起動して確かめます。
# 京都の野菜の週ごとの出荷量(架空のデータ)
set.seed(1234)
farmData <- data.frame(
日付 = rep(seq(as.Date("2026-04-01"), by = "week", length.out = 12), times = 3),
品目 = rep(c("九条ねぎ", "賀茂なす", "万願寺とうがらし"), each = 12),
産地 = sample(c("伏見区", "亀岡市", "京丹後市"), 36, replace = TRUE),
出荷量 = round(c(rnorm(12, 120, 15), rnorm(12, 80, 20), rnorm(12, 95, 10)))
)
head(farmData)
日付 品目 産地 出荷量
1 2026-04-01 九条ねぎ 亀岡市 120
2 2026-04-08 九条ねぎ 亀岡市 106
3 2026-04-15 九条ねぎ 伏見区 137
4 2026-04-22 九条ねぎ 京丹後市 113
5 2026-04-29 九条ねぎ 伏見区 109
6 2026-05-06 九条ねぎ 伏見区 112ラベルにツールチップを付ける:addTooltipコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| label | ラベル。文字列かshinyのタグ | なし |
| tooltipText | マウスを重ねたときに表示する文。NAかNULLでツールチップを付けず、ラベルをそのまま返す | なし |
# 「出荷量」のラベルに単位の説明を付ける
addTooltip("出荷量", "単位はkgです。直売所の伝票から集計しています")
[[1]]
<div class="container-fluid">
<bslib-tooltip placement="auto" bsOptions="[]" data-require-bs-version="5" data-require-bs-caller="tooltip()">
<template>単位はkgです。直売所の伝票から集計しています</template>
出荷量
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-info-circle " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"></path></svg>
</bslib-tooltip>
</div>
# NAを渡すとツールチップを付けず、ラベルをそのまま返す
addTooltip("出荷量", NA)
[1] "出荷量"説明アイコン付きの文を置く:navSpanTextコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| text | 表示する文 | なし |
| tooltipText | アイコンにマウスを重ねたときに表示する文 | NA |
# 集計の範囲を、文の後ろの説明アイコンで補う
navSpanText("2026年度 春の出荷",
tooltipText = "4月から6月に伏見区・亀岡市・京丹後市の直売所へ出荷された分です"
)
<span>
2026年度 春の出荷
<bslib-tooltip placement="bottom" bsOptions="[]" data-require-bs-version="5" data-require-bs-caller="tooltip()">
<template>4月から6月に伏見区・亀岡市・京丹後市の直売所へ出荷された分です</template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-info-circle " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"></path></svg>
</bslib-tooltip>
</span>選択メニューを作る:navSelectコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| inputId | 入力欄のID。serverではinput$<inputId>で読む | なし |
| label | 入力欄の上か横に表示するラベル | なし |
| choices | 選択肢。NULLのままにして、後からserverでshiny::updateSelectizeInput()で入れることもできる | NULL |
| selected | 最初に選ばれている選択肢。初期値は1つ目の選択肢 | choices[1] |
| multiple | TRUEで複数を選べるようにする | FALSE |
| create | TRUEで選択肢に無い値を入力できるようにする | FALSE |
| tooltipText | ラベルに付ける説明のツールチップ | NA |
# 品目を複数選べるメニュー。最初は九条ねぎと賀茂なすを選んでおく
navSelect("item", "品目",
choices = unique(farmData$品目),
selected = c("九条ねぎ", "賀茂なす"),
multiple = TRUE,
tooltipText = "複数の品目を重ねて比べられます"
)
<div class="form-group shiny-input-container" style="width:100%;">
<label class="control-label" id="item-label" for="item">
<bslib-tooltip placement="auto" bsOptions="[]" data-require-bs-version="5" data-require-bs-caller="tooltip()">
<template>複数の品目を重ねて比べられます</template>
品目
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-info-circle " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"></path></svg>
</bslib-tooltip>
</label>
<div>
<select class="shiny-input-select form-control" id="item" multiple="multiple"><option value="九条ねぎ" selected>九条ねぎ</option>
<option value="賀茂なす" selected>賀茂なす</option>
<option value="万願寺とうがらし">万願寺とうがらし</option></select>
<script type="application/json" data-for="item">{"create":false,"plugins":["selectize-plugin-a11y"]}</script>
</div>
</div>
# 産地のメニュー。一覧に無い産地も入力できるようにする
navSelect("area", "産地",
choices = c("伏見区", "亀岡市", "京丹後市"),
create = TRUE
)
<div class="form-group shiny-input-container" style="width:100%;">
<label class="control-label" id="area-label" for="area">産地</label>
<div>
<select class="shiny-input-select form-control" id="area"><option value="伏見区" selected>伏見区</option>
<option value="亀岡市">亀岡市</option>
<option value="京丹後市">京丹後市</option></select>
<script type="application/json" data-for="area">{"create":true,"plugins":["selectize-plugin-a11y"]}</script>
</div>
</div>数値の入力欄を作る:navNumericコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| inputId | 入力欄のID。serverではinput$<inputId>で読む | なし |
| label | 入力欄の上か横に表示するラベル | なし |
| value | 最初に入っている数値 | 1 |
| min | 入力できる最小値。NAで制限なし | NA |
| max | 入力できる最大値。NAで制限なし | NA |
| tooltipText | ラベルに付ける説明のツールチップ | NA |
# 出荷量の下限。0から200kgの間で入力させる
navNumeric("minAmount", "出荷量の下限(kg)",
value = 50, min = 0, max = 200,
tooltipText = "この値より少ない週は集計から外します"
)
<div class="form-group shiny-input-container" style="width:100%;">
<label class="control-label" id="minAmount-label" for="minAmount">
<bslib-tooltip placement="auto" bsOptions="[]" data-require-bs-version="5" data-require-bs-caller="tooltip()">
<template>この値より少ない週は集計から外します</template>
出荷量の下限(kg)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-info-circle " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"></path></svg>
</bslib-tooltip>
</label>
<input id="minAmount" type="number" class="shiny-input-number form-control" value="50" data-update-on="change" min="0" max="200"/>
</div>日付の入力欄を作る:navDateコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| inputId | 入力欄のID。serverではinput$<inputId>で読む | なし |
| label | 入力欄の上か横に表示するラベル | なし |
| range | TRUEで開始日と終了日を選ぶ。FALSEで1日だけ選ぶ | FALSE |
| tooltipText | ラベルに付ける説明のツールチップ | NA |
# 集計期間を開始日と終了日で選ばせる
navDate("period", "集計期間", range = TRUE)
<div class="form-group shiny-input-container">
<label class="control-label" id="period-label" for="period">集計期間</label>
<input id="period" class="sw-air-picker form-control" type="text" autocomplete="off"/>
<script type="application/json" data-for="period">{"updateOn":"change","todayButtonAsDate":false,"language":"EN","options":{"autoClose":false,"timepicker":false,"range":true,"dateFormat":"yyyy-MM-dd","multipleDates":false,"multipleDatesSeparator":" to ","view":"days","minView":"days","clearButton":true,"todayButton":false,"buttons":"clear","monthsField":"monthsShort","onlyTimepicker":false,"toggleSelected":true}}</script>
</div>
# 1日だけ選ばせる
navDate("shipDay", "出荷日")
<div class="form-group shiny-input-container">
<label class="control-label" id="shipDay-label" for="shipDay">出荷日</label>
<input id="shipDay" class="sw-air-picker form-control" type="text" autocomplete="off"/>
<script type="application/json" data-for="shipDay">{"updateOn":"change","todayButtonAsDate":false,"language":"EN","options":{"autoClose":false,"timepicker":false,"range":false,"dateFormat":"yyyy-MM-dd","multipleDates":false,"multipleDatesSeparator":" to ","view":"days","minView":"days","clearButton":true,"todayButton":false,"buttons":"clear","monthsField":"monthsShort","onlyTimepicker":false,"toggleSelected":true}}</script>
</div>このほかの入力欄も、inputId、labelの順で同じように書けます。ボタンはnavButton、ファイルの読み込みはnavUpload、ダウンロードはnavDownload、チェックボックスはnavCheckbox、文字の入力はnavText、色の選択はnavColorです。
プロットの出力欄を作る:navOutputPlotコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| outputId | 出力欄のID。serverでoutput$<outputId> <- render…()として中身を入れる | なし |
| label | 出力欄の上に表示する見出し | NULL |
| tooltipText | 見出しの後ろに付ける説明のツールチップ | NA |
# 週ごとの出荷量のプロットを置く欄。見出しに説明を付ける
navOutputPlot("amountPlot",
label = "週ごとの出荷量",
tooltipText = "選んだ品目を色分けしてプロットします"
)
<div class="ezr-output-label" style="font-weight: 500; margin-bottom: 0.25rem;">
<bslib-tooltip placement="auto" bsOptions="[]" data-require-bs-version="5" data-require-bs-caller="tooltip()">
<template>選んだ品目を色分けしてプロットします</template>
週ごとの出荷量
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-info-circle " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"></path></svg>
</bslib-tooltip>
</div>
<div class="shiny-plot-output html-fill-item" id="amountPlot" style="width:80%;height:400px;"></div>表はnavOutputTable、文字はnavOutputText、plotlyのグラフはnavOutputPlotly、ggiraphのグラフはnavOutputGirafe、画像はnavOutputPicで、オプションはnavOutputPlotと同じです。
1段のタブを作る:singleTabコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| title | ナビゲーションバーに表示するタブの名前 | なし |
| … | タブの中に並べる入力欄・出力欄 | なし |
| icon | タブの名前の前に付けるアイコン。bsicons::bs_icon(“table”)など | NULL |
| height | タブの中身を入れるカードの高さ。options(EZRShiny.cardHeight = “70vh”)で全タブの初期値を変えられる | cardHeight() |
# 「出荷の推移」タブ。品目のメニューとプロットの欄を上から並べる
trendTab <- singleTab("出荷の推移",
navSelect("item", "品目", choices = unique(farmData$品目), multiple = TRUE),
navOutputPlot("amountPlot", label = "週ごとの出荷量"),
icon = bsicons::bs_icon("graph-up")
)サブタブを持つタブを作る:biLevelTabコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| title | ナビゲーションバーに表示するタブの名前 | なし |
| … | 中に並べるサブタブ。subTabで作る | なし |
| icon | タブの名前の前に付けるアイコン。bsicons::bs_icon(“table”)など | NULL |
| id | サブタブの並びのID。showNavTabsとhideNavTabsで使う。初期値はtitleから空白を除いたもの | title |
| height | タブの中身を入れるカードの高さ。options(EZRShiny.cardHeight = “70vh”)で全タブの初期値を変えられる | cardHeight() |
# 「産地別」タブ。集計ボタンのサブタブと、一覧表のサブタブを持つ
areaTab <- biLevelTab("産地別",
subTab("集計",
navNumeric("minAmount", "出荷量の下限(kg)", value = 50, min = 0, max = 200),
navButton("calc", "産地ごとに集計する"),
value = "calcTab"
),
subTab("一覧表",
navOutputTable("areaTable"),
value = "tableTab"
),
id = "areaTabs"
)ナビゲーションバー付きの画面を組み立てる:UINavコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| … | ナビゲーションバーに並べるタブ | なし |
| logoFile | ナビゲーションバーの左に表示する画像のファイル名。左から順に並ぶ。画像はアプリのwwwフォルダに置き、”logo.png”のようにファイル名だけを書く(png、svg、jpg、gif)。NULLか””でロゴなし | NULL |
| appName | ロゴの後ろに表示するアプリ名。NULLのときはグローバル変数appNameを使い、無ければ名前なし | NULL |
| logoHeight | ロゴの高さ。CSSの長さで指定し、全てのロゴに同じ値を使う | “40vh” |
| barColor | ナビゲーションバーの背景色。色の名前か”#005596″のような16進数。NULLでテーマの色。文字の色は読みやすいように明暗が切り替わる | NULL |
| theme | アプリ全体の見た目。bslib::bs_theme(primary = “#005596”, preset = “cosmo”)のように指定する | bslib::bs_theme() |
# 2つのタブをナビゲーションバーに並べ、バーを京紫に近い色にする
ui <- UINav(
trendTab,
areaTab,
appName = "京都やさい出荷ダッシュボード",
barColor = "#5B3A8C",
theme = bslib::bs_theme(primary = "#5B3A8C")
)
<おすすめのRに関する書籍です>
入力欄を使えなくする:deactivateItemsコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| itemIDs | 切り替える入力欄のID。文字ベクトルで指定する | なし |
serverの中で使います。使えるように戻すときはactivateItemsで、オプションは同じです。
# 下限が0のときは集計ボタンを押せなくする
buttonServer <- function(input, output, session) {
shiny::observe({
if (isTRUE(input$minAmount > 0)) {
activateItems("calc")
} else {
deactivateItems("calc")
}
})
}サブタブを隠す:hideNavTabsコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| rootID | タブの並びのID | なし |
| tabIDs | 隠す、または表示するタブのID。文字ベクトルで指定する | なし |
serverの中で使います。隠したタブを表示するときはshowNavTabsで、オプションは同じです。
# 集計するまでは一覧表のサブタブを隠しておき、ボタンが押されたら表示する
tabServer <- function(input, output, session) {
hideNavTabs("areaTabs", "tableTab")
shiny::observeEvent(input$calc, {
showNavTabs("areaTabs", "tableTab")
})
}ここまでの部品をつないで、アプリを起動します。
UIのタブ切り替えと表示制御:showNavTabsコマンド
# serverで、プロット・一覧表の中身と、ボタン・タブの切り替えをまとめる
server <- function(input, output, session) {
hideNavTabs("areaTabs", "tableTab")
# 下限が0のときは集計ボタンを押せなくする
shiny::observe({
if (isTRUE(input$minAmount > 0)) {
activateItems("calc")
} else {
deactivateItems("calc")
}
})
# 選んだ品目の出荷量を週ごとにプロット
output$amountPlot <- shiny::renderPlot({
shiny::req(input$item)
useData <- farmData[farmData$品目 %in% input$item, ]
plot(出荷量 ~ 日付,
data = useData, type = "n",
xlab = "日付", ylab = "出荷量(kg)"
)
for (i in seq_along(input$item)) {
oneItem <- useData[useData$品目 == input$item[i], ]
lines(oneItem$日付, oneItem$出荷量, col = i, lwd = 2)
}
legend("topright", legend = input$item, col = seq_along(input$item), lwd = 2)
})
# ボタンが押されたら、下限以上の週だけを産地ごとに合計して表示する
areaSum <- shiny::eventReactive(input$calc, {
useData <- farmData[farmData$出荷量 >= input$minAmount, ]
aggregate(出荷量 ~ 産地 + 品目, data = useData, FUN = sum)
})
output$areaTable <- DT::renderDT(areaSum())
shiny::observeEvent(input$calc, {
showNavTabs("areaTabs", "tableTab")
})
}
# RStudioなどで実行したときにアプリを起動する
if (interactive()) {
shiny::shinyApp(ui, server)
}同梱のサンプルアプリを起動する:runExampleAppコマンド
| オプション | 意味 | 初期値 |
|---|---|---|
| … | shiny::runApp()に渡すオプション。launch.browser = TRUEなど | なし |
# サンプルアプリを作っているファイルの一覧
list.files(system.file("examples", "titanicExplorer", package = "EZRShiny"),
recursive = TRUE
)
[1] "app.R"
[2] "Functions/1-Read_In_Data_Functions.R"
[3] "Functions/2-Summary_Functions.R"
[4] "Functions/3-Plot_Functions.R"
[5] "Necessary_Files/titanic.csv"
[6] "www/logo.svg"
# RStudioなどで実行したときに、ブラウザでサンプルアプリを開く
if (interactive()) {
runExampleApp(launch.browser = TRUE)
}サンプルアプリは、app.Rと、データの読み込み・集計・プロットのコマンドを分けたFunctionsフォルダでできています。自分のアプリをファイルに分けて書くときの見本になります。
<おすすめのRに関する書籍です>
この記事が誰かの役に立ちますように。