update
This commit is contained in:
216
samplesize/diagnostest/app.R
Normal file
216
samplesize/diagnostest/app.R
Normal file
@@ -0,0 +1,216 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO NGHIÊN CỨU GIÁ TRỊ CHẨN ĐOÁN
|
||||
# - Bao gồm tính toán cỡ mẫu và phân tích ảnh hưởng của tỷ lệ hiện mắc.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Nghiên Cứu Giá trị Chẩn đoán"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
|
||||
h5("Thông số Xét nghiệm Dự kiến"),
|
||||
sliderInput("sens", "Độ nhạy (Sensitivity) dự kiến:", value = 0.90, min = 0.5, max = 0.99, step = 0.01),
|
||||
sliderInput("spec", "Độ đặc hiệu (Specificity) dự kiến:", value = 0.98, min = 0.5, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
h5("Thông số Quần thể & Độ chính xác"),
|
||||
sliderInput("prev", "Tỷ lệ hiện mắc (Prevalence) của bệnh:", value = 0.02, min = 0.001, max = 0.99, step = 0.001),
|
||||
sliderInput("d", "Độ chính xác mong muốn (Sai số d):", value = 0.05, min = 0.01, max = 0.15, step = 0.005),
|
||||
sliderInput("conf_level", "Độ tin cậy:", value = 0.95, min = 0.80, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
actionButton("go_diag", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "diag_results_tabs",
|
||||
type = "pills",
|
||||
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("diag_result_text"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Phân tích & Đồ thị",
|
||||
withSpinner(plotOutput("diag_prevalence_plot"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giải thích Tham số",
|
||||
uiOutput("diag_params_ui")),
|
||||
|
||||
tabPanel("Công thức & Ví dụ",
|
||||
h4("Công thức tính toán"),
|
||||
p("Quá trình tính toán gồm 2 bước:"),
|
||||
p(strong("Bước 1: Tính số người có bệnh (\\(n_{dis}\\)) và không bệnh (\\(n_{hea}\\)) cần thiết")),
|
||||
withMathJax(HTML("Công thức chung: $$n = \\frac{Z^2_{1-\\alpha/2} \\cdot p(1-p)}{d^2}$$")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("Để tính \\(n_{dis}\\), thay \\(p\\) bằng Độ nhạy dự kiến (Se).")),
|
||||
withMathJax(tags$li("Để tính \\(n_{hea}\\), thay \\(p\\) bằng Độ đặc hiệu dự kiến (Sp)."))
|
||||
),
|
||||
|
||||
p(strong("Bước 2: Tính tổng cỡ mẫu (N) cần sàng lọc từ tỷ lệ hiện mắc (Prevalence)")),
|
||||
withMathJax(HTML("$$N_{sens} = \\frac{n_{dis}}{\\text{Prevalence}}$$")),
|
||||
withMathJax(HTML("$$N_{spec} = \\frac{n_{hea}}{1 - \\text{Prevalence}}$$")),
|
||||
withMathJax(HTML("Cỡ mẫu cuối cùng là giá trị lớn hơn: $$N = \\max(N_{sens}, N_{spec})$$")),
|
||||
|
||||
hr(),
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn đánh giá độ chính xác của một xét nghiệm nhanh mới để phát hiện bệnh lao (TB) tại cộng đồng."),
|
||||
tags$ul(
|
||||
tags$li(strong("Độ nhạy & đặc hiệu dự kiến:"), withMathJax(HTML(" Dựa trên y văn, họ kỳ vọng Se = 90% và Sp = 98%."))),
|
||||
tags$li(strong("Độ chính xác mong muốn (d):"), withMathJax(HTML(" Khoảng tin cậy 95% cho các ước tính này không được rộng hơn \\(\\pm 5\\)% (d=0.05)."))),
|
||||
tags$li(strong("Tỷ lệ hiện mắc (Prevalence):"), withMathJax(HTML(" Tỷ lệ mắc lao trong vùng nghiên cứu là 2% (Prevalence = 0.02).")))
|
||||
),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_diag <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_diag, {
|
||||
# Tính toán cơ bản
|
||||
z_alpha <- qnorm(1 - (1 - input$conf_level) / 2)
|
||||
|
||||
n_disease <- ceiling((z_alpha^2 * input$sens * (1 - input$sens)) / input$d^2)
|
||||
n_healthy <- ceiling((z_alpha^2 * input$spec * (1 - input$spec)) / input$d^2)
|
||||
|
||||
n_from_sens <- ceiling(n_disease / input$prev)
|
||||
n_from_spec <- ceiling(n_healthy / (1 - input$prev))
|
||||
|
||||
n_total <- max(n_from_sens, n_from_spec)
|
||||
|
||||
rv_diag$n_disease <- n_disease
|
||||
rv_diag$n_healthy <- n_healthy
|
||||
rv_diag$N <- n_total
|
||||
|
||||
# Dữ liệu cho biểu đồ phân tích độ nhạy
|
||||
prev_range <- seq(0.001, 0.99, length.out = 200)
|
||||
|
||||
plot_data <- tibble(
|
||||
prevalence = prev_range,
|
||||
N_from_sens = ceiling(n_disease / prevalence),
|
||||
N_from_spec = ceiling(n_healthy / (1 - prevalence))
|
||||
) %>%
|
||||
mutate(N_total = pmax(N_from_sens, N_from_spec))
|
||||
|
||||
rv_diag$plot_data <- plot_data
|
||||
})
|
||||
|
||||
output$diag_result_text <- renderUI({
|
||||
if (input$go_diag == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_diag$N)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán"),
|
||||
p("Dựa trên các tham số bạn đã nhập, để đạt được độ chính xác mong muốn, nghiên cứu của bạn cần:"),
|
||||
|
||||
fluidRow(
|
||||
column(6, tags$div(class="alert alert-warning", style="text-align: center;",
|
||||
h5("Số người CÓ BỆNH tối thiểu"),
|
||||
h3(rv_diag$n_disease)
|
||||
)),
|
||||
column(6, tags$div(class="alert alert-success", style="text-align: center;",
|
||||
h5("Số người KHÔNG BỆNH tối thiểu"),
|
||||
h3(rv_diag$n_healthy)
|
||||
))
|
||||
),
|
||||
|
||||
hr(),
|
||||
h4("Tổng số người cần sàng lọc"),
|
||||
p(paste0("Với tỷ lệ hiện mắc của bệnh là ", scales::percent(input$prev, accuracy = 0.1),
|
||||
", để tìm đủ số lượng người bệnh và không bệnh nói trên, bạn cần phải sàng lọc tổng cộng:")),
|
||||
|
||||
tags$h2(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_diag$N, " người")
|
||||
)
|
||||
})
|
||||
|
||||
output$diag_prevalence_plot <- renderPlot({
|
||||
req(rv_diag$plot_data)
|
||||
|
||||
# Giới hạn trục y để dễ nhìn hơn
|
||||
max_y <- min(quantile(rv_diag$plot_data$N_total, 0.99, na.rm=TRUE) * 1.2, max(rv_diag$plot_data$N_total, na.rm=TRUE))
|
||||
if(is.infinite(max_y)) max_y <- 5 * rv_diag$N
|
||||
|
||||
|
||||
ggplot(rv_diag$plot_data, aes(x = prevalence)) +
|
||||
geom_line(aes(y = N_from_sens, color = "Dựa trên Độ nhạy"), size = 1.2) +
|
||||
geom_line(aes(y = N_from_spec, color = "Dựa trên Độ đặc hiệu"), size = 1.2) +
|
||||
geom_vline(xintercept = input$prev, linetype = "dashed", color = "red", size = 1) +
|
||||
annotate("text", x = input$prev, y = max_y * 0.9,
|
||||
label = paste0("Tỷ lệ hiện mắc đã chọn\n(", scales::percent(input$prev, accuracy = 0.1), ")"),
|
||||
color = "red", hjust = if_else(input$prev > 0.5, 1.1, -0.1)) +
|
||||
scale_color_manual(values = c("Dựa trên Độ nhạy" = "orange", "Dựa trên Độ đặc hiệu" = "seagreen")) +
|
||||
scale_x_continuous(labels = scales::percent) +
|
||||
scale_y_log10(labels = scales::comma) + # Trục log giúp dễ nhìn hơn với các giá trị lớn
|
||||
coord_cartesian(ylim = c(10, max_y)) +
|
||||
labs(
|
||||
title = "Ảnh hưởng của Tỷ lệ hiện mắc lên Tổng Cỡ mẫu (N)",
|
||||
subtitle = "Đường cong cho thấy số người cần sàng lọc để đạt đủ số ca bệnh (cam) hoặc ca không bệnh (xanh)",
|
||||
x = "Tỷ lệ hiện mắc (Prevalence)",
|
||||
y = "Tổng Cỡ mẫu cần sàng lọc (trục Log)",
|
||||
color = "Cỡ mẫu yêu cầu:"
|
||||
) +
|
||||
theme_minimal(base_size = 14) +
|
||||
theme(legend.position = "bottom")
|
||||
})
|
||||
|
||||
output$diag_params_ui <- renderUI({
|
||||
tagList(
|
||||
h4("Giải thích các tham số chính"),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Độ nhạy (Sensitivity)"),
|
||||
p("Là khả năng của xét nghiệm phát hiện chính xác những người ", tags$b("thực sự có bệnh."), " Nó được tính bằng (Số ca dương tính thật) / (Tổng số người có bệnh). Một độ nhạy 90% nghĩa là xét nghiệm sẽ phát hiện được 90 trong số 100 người có bệnh.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Độ đặc hiệu (Specificity)"),
|
||||
p("Là khả năng của xét nghiệm xác định chính xác những người ", tags$b("thực sự không có bệnh."), " Nó được tính bằng (Số ca âm tính thật) / (Tổng số người không có bệnh). Một độ đặc hiệu 98% nghĩa là xét nghiệm sẽ cho kết quả âm tính đúng cho 98 trong số 100 người không có bệnh.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Tỷ lệ hiện mắc (Prevalence)"),
|
||||
p("Là tỷ lệ phần trăm người đang mắc bệnh trong một quần thể tại một thời điểm nhất định. Tham số này ", tags$strong("cực kỳ quan trọng"), " vì nó quyết định bạn phải sàng lọc bao nhiêu người để tìm đủ số ca bệnh cần thiết cho nghiên cứu, đặc biệt là với các bệnh hiếm.")
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
187
samplesize/equivalence/app.R
Normal file
187
samplesize/equivalence/app.R
Normal file
@@ -0,0 +1,187 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO NGHIÊN CỨU TƯƠNG ĐƯƠNG (EQUIVALENCE)
|
||||
# - So sánh hai giá trị trung bình bằng phương pháp Two One-Sided Tests (TOST).
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu: Nghiên cứu Tương đương (So sánh 2 Trung bình)"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
|
||||
h5("Tham số Hiệu ứng & Biến thiên"),
|
||||
numericInput("delta_eq", "Biên tương đương (±δ):", value = 5, min = 0),
|
||||
numericInput("sd_eq", "Độ lệch chuẩn chung (σ):", value = 15, min = 0),
|
||||
numericInput("diff_eq", "Khác biệt trung bình kỳ vọng (μT - μS):", value = 0),
|
||||
helpText("Giá trị 0 có nghĩa là giả định hai nhóm có hiệu quả chính xác như nhau."),
|
||||
|
||||
hr(),
|
||||
h5("Tham số Kiểm định"),
|
||||
sliderInput("alpha_eq", "Mức ý nghĩa (α, cho mỗi phía):", value = 0.05, min = 0.01, max = 0.1, step = 0.01),
|
||||
sliderInput("power_eq", "Công suất mong muốn (1 - β):", value = 0.8, min = 0.5, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
actionButton("go_eq", "Tính toán Cỡ mẫu", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "eq_results_tabs",
|
||||
type = "pills",
|
||||
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("eq_result_text"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Phân tích & Đồ thị",
|
||||
withSpinner(plotOutput("eq_margin_plot"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giải thích Tham số",
|
||||
uiOutput("eq_params_ui")),
|
||||
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê: Two One-Sided Tests (TOST)"),
|
||||
p("Để chứng minh tương đương, chúng ta phải bác bỏ đồng thời CẢ HAI giả thuyết không sau đây:"),
|
||||
p(strong("1. Giả thuyết H01: Can thiệp mới thua kém đáng kể.")),
|
||||
withMathJax(HTML("$$H_{01}: \\mu_T - \\mu_S \\le -\\delta$$")),
|
||||
p(strong("2. Giả thuyết H02: Can thiệp mới vượt trội đáng kể.")),
|
||||
withMathJax(HTML("$$H_{02}: \\mu_T - \\mu_S \\ge +\\delta$$")),
|
||||
p("Chỉ khi bác bỏ được cả hai, chúng ta mới có thể chấp nhận giả thuyết thay thế \\(H_a\\):"),
|
||||
withMathJax(HTML("$$H_a: -\\delta < \\mu_T - \\mu_S < +\\delta$$")),
|
||||
|
||||
hr(),
|
||||
h4("Công thức tính cỡ mẫu (mỗi nhóm)"),
|
||||
p("Công thức cho kiểm định tương đương, giả định khác biệt kỳ vọng là 0:"),
|
||||
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{\\delta^2}$$")),
|
||||
p("Công thức tổng quát hơn khi khác biệt kỳ vọng \\(|\\mu_T - \\mu_S| > 0\\):"),
|
||||
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{(\\delta - |\\mu_T - \\mu_S|)^2}$$")),
|
||||
|
||||
hr(),
|
||||
h4("Ví dụ Y tế công cộng (Nghiên cứu Tương đương sinh học)"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một công ty dược phẩm sản xuất một phiên bản thuốc generic (T) của một loại thuốc hạ huyết áp nổi tiếng (S). Họ cần chứng minh rằng thuốc generic có hiệu quả tương đương sinh học với thuốc gốc. Tiêu chí tương đương được định nghĩa là sự khác biệt trong mức giảm huyết áp trung bình phải nằm trong khoảng \\(\\pm 5\\) mmHg."),
|
||||
p(strong("Tham số:")),
|
||||
tags$ul(
|
||||
tags$li("Biên tương đương \\(\\delta = 5\\) mmHg."),
|
||||
tags$li("Độ lệch chuẩn \\(\\sigma = 15\\) mmHg."),
|
||||
tags$li("Kỳ vọng sự khác biệt là 0."),
|
||||
tags$li("Mức ý nghĩa \\(\\alpha = 0.05\\) và công suất 80%.")
|
||||
),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_eq <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_eq, {
|
||||
# Validate input: Khác biệt kỳ vọng phải nhỏ hơn biên tương đương
|
||||
validate(
|
||||
need(abs(input$diff_eq) < input$delta_eq,
|
||||
"Lỗi: Khác biệt trung bình kỳ vọng phải nằm trong biên tương đương ( |μT - μS| < δ ). Nếu không, không thể chứng minh tương đương.")
|
||||
)
|
||||
|
||||
# Tính toán
|
||||
z_alpha <- qnorm(1 - input$alpha_eq)
|
||||
z_beta <- qnorm(input$power_eq) # Với TOST, thường dùng Z_beta thay vì Z_{beta/2}
|
||||
|
||||
numerator <- 2 * (input$sd_eq^2) * (z_alpha + z_beta)^2
|
||||
denominator <- (input$delta_eq - abs(input$diff_eq))^2
|
||||
|
||||
n_per_group <- ceiling(numerator / denominator)
|
||||
|
||||
rv_eq$n <- n_per_group
|
||||
|
||||
# Dữ liệu cho biểu đồ
|
||||
margin_range <- seq(abs(input$diff_eq) + 0.1, input$delta_eq * 1.5, length.out = 100)
|
||||
|
||||
plot_data <- data.frame(
|
||||
margin = margin_range,
|
||||
n = ceiling(2 * (input$sd_eq^2) * (z_alpha + z_beta)^2 / (margin_range - abs(input$diff_eq))^2)
|
||||
)
|
||||
rv_eq$plot_data <- plot_data
|
||||
})
|
||||
|
||||
output$eq_result_text <- renderUI({
|
||||
if (input$go_eq == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán Cỡ mẫu' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_eq$n)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán cỡ mẫu"),
|
||||
p("Với các tham số đã cho, để chứng minh tính tương đương, nghiên cứu của bạn cần:"),
|
||||
|
||||
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_eq$n, " người tham gia MỖI NHÓM"),
|
||||
p(style = "text-align: center; font-size: 1.2em;",
|
||||
"Tổng cỡ mẫu cần thiết: ", tags$b(rv_eq$n * 2)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
output$eq_margin_plot <- renderPlot({
|
||||
req(rv_eq$plot_data)
|
||||
|
||||
ggplot(rv_eq$plot_data, aes(x = margin, y = n)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_vline(xintercept = input$delta_eq, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = "Ảnh hưởng của Biên tương đương (δ) lên Cỡ mẫu",
|
||||
subtitle = "Biên càng lớn (khoảng tương đương rộng hơn), cỡ mẫu yêu cầu càng nhỏ",
|
||||
x = "Biên tương đương (δ)",
|
||||
y = "Cỡ mẫu mỗi nhóm (n)"
|
||||
) +
|
||||
annotate("text", x = input$delta_eq * 1.05, y = max(rv_eq$plot_data$n) * 0.9,
|
||||
label = paste0("Biên đã chọn\nδ = ", input$delta_eq),
|
||||
color = "red", hjust = 0) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$eq_params_ui <- renderUI({
|
||||
tagList(
|
||||
h4("Giải thích các tham số chính"),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Biên tương đương (Equivalence Margin - ±δ)"),
|
||||
p("Đây là tham số định nghĩa 'vùng tương đương'. Nó xác định một khoảng chênh lệch (từ ", tags$b("-δ"), " đến ", tags$b("+δ"), ") mà trong đó, sự khác biệt giữa hai can thiệp được coi là không có ý nghĩa về mặt lâm sàng. Việc lựa chọn δ phải dựa trên cơ sở khoa học vững chắc.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Phương pháp Two One-Sided Tests (TOST)"),
|
||||
p("Đây là phương pháp thống kê tiêu chuẩn để chứng minh tương đương. Thay vì một kiểm định hai phía thông thường (nhằm chứng minh sự khác biệt), TOST thực hiện hai kiểm định một phía để chứng minh ", tags$strong("sự thiếu vắng một sự khác biệt có ý nghĩa."), " Bạn phải chứng minh rằng khoảng tin cậy 90% (tương ứng với hai kiểm định một phía tại α=0.05) của sự khác biệt nằm hoàn toàn bên trong biên tương đương [-\\(\\delta\\), +\\(\\delta\\)].")
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
185
samplesize/non-inferiority/app.R
Normal file
185
samplesize/non-inferiority/app.R
Normal file
@@ -0,0 +1,185 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO NGHIÊN CỨU KHÔNG THUA KÉM (NON-INFERIORITY)
|
||||
# - So sánh hai giá trị trung bình.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu: Nghiên cứu Không thua kém (So sánh 2 Trung bình)"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
|
||||
h5("Tham số Hiệu ứng & Biến thiên"),
|
||||
numericInput("delta", "Biên không thua kém (δ):", value = 5, min = 0),
|
||||
numericInput("sd", "Độ lệch chuẩn chung (σ):", value = 15, min = 0),
|
||||
numericInput("diff", "Khác biệt trung bình kỳ vọng (μT - μS):", value = 0),
|
||||
helpText("Giá trị 0 có nghĩa là giả định hai nhóm có hiệu quả tương đương."),
|
||||
|
||||
hr(),
|
||||
h5("Tham số Kiểm định"),
|
||||
sliderInput("alpha_ni", "Mức ý nghĩa (α, một phía):", value = 0.05, min = 0.01, max = 0.1, step = 0.01),
|
||||
sliderInput("power_ni", "Công suất mong muốn (1 - β):", value = 0.8, min = 0.5, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
actionButton("go_ni", "Tính toán Cỡ mẫu", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "ni_results_tabs",
|
||||
type = "pills",
|
||||
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("ni_result_text"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Phân tích & Đồ thị",
|
||||
withSpinner(plotOutput("ni_margin_plot"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giải thích Tham số",
|
||||
uiOutput("ni_params_ui")),
|
||||
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
p("Mục tiêu là chứng minh can thiệp mới (Test) không thua kém can thiệp chuẩn (Standard) một cách đáng kể."),
|
||||
p(strong("Giả thuyết không (H0): Can thiệp mới THUA KÉM."), "Sự khác biệt hiệu quả lớn hơn hoặc bằng một ngưỡng \\(\\delta\\) đã định trước."),
|
||||
withMathJax(HTML("$$H_0: \\mu_S - \\mu_T \\ge \\delta$$")),
|
||||
p(strong("Giả thuyết thay thế (Ha): Can thiệp mới KHÔNG THUA KÉM.")),
|
||||
withMathJax(HTML("$$H_a: \\mu_S - \\mu_T < \\delta$$")),
|
||||
|
||||
hr(),
|
||||
h4("Công thức tính cỡ mẫu (mỗi nhóm)"),
|
||||
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{(\\mu_T - \\mu_S - \\delta)^2}$$")),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("\\(\\delta\\) là biên không thua kém (non-inferiority margin).")),
|
||||
withMathJax(tags$li("\\(\\sigma\\) là độ lệch chuẩn chung của kết quả.")),
|
||||
withMathJax(tags$li("\\(Z_{\\alpha}\\) là Z-score một phía (ví dụ: 1.645 cho \\(\\alpha=0.05\\)).")),
|
||||
withMathJax(tags$li("\\(Z_{\\beta}\\) là Z-score của công suất (ví dụ: 0.84 cho power=80%).")),
|
||||
withMathJax(tags$li("\\(\\mu_T - \\mu_S\\) là sự khác biệt trung bình thực sự kỳ vọng giữa hai nhóm."))
|
||||
),
|
||||
|
||||
hr(),
|
||||
h4("Ví dụ Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một thuốc hạ huyết áp mới (T) được kỳ vọng có hiệu quả tương tự thuốc chuẩn (S) nhưng dễ sử dụng hơn. Các nhà lâm sàng đồng ý rằng nếu thuốc T làm giảm huyết áp kém hơn không quá 5 mmHg so với thuốc S, nó vẫn được coi là chấp nhận được (không thua kém). Độ lệch chuẩn của thay đổi huyết áp là 15 mmHg."),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_ni <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_ni, {
|
||||
# Tính toán
|
||||
z_alpha <- qnorm(1 - input$alpha_ni)
|
||||
z_beta <- qnorm(input$power_ni)
|
||||
|
||||
numerator <- 2 * (input$sd^2) * (z_alpha + z_beta)^2
|
||||
denominator <- (input$diff - input$delta)^2
|
||||
|
||||
validate(
|
||||
need(denominator > 0, "Lỗi: Mẫu số của công thức bằng 0. Khác biệt kỳ vọng không thể bằng biên không thua kém.")
|
||||
)
|
||||
|
||||
n_per_group <- ceiling(numerator / denominator)
|
||||
|
||||
rv_ni$n <- n_per_group
|
||||
|
||||
# Dữ liệu cho biểu đồ
|
||||
margin_range <- seq(input$delta * 0.5, input$delta * 1.5, length.out = 100)
|
||||
|
||||
plot_data <- data.frame(
|
||||
margin = margin_range,
|
||||
n = ceiling(2 * (input$sd^2) * (z_alpha + z_beta)^2 / (input$diff - margin_range)^2)
|
||||
)
|
||||
rv_ni$plot_data <- plot_data
|
||||
})
|
||||
|
||||
output$ni_result_text <- renderUI({
|
||||
if (input$go_ni == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán Cỡ mẫu' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_ni$n)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán cỡ mẫu"),
|
||||
p("Với các tham số đã cho, để chứng minh tính không thua kém, nghiên cứu của bạn cần:"),
|
||||
|
||||
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_ni$n, " người tham gia MỖI NHÓM"),
|
||||
p(style = "text-align: center; font-size: 1.2em;",
|
||||
"Tổng cỡ mẫu cần thiết: ", tags$b(rv_ni$n * 2)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
output$ni_margin_plot <- renderPlot({
|
||||
req(rv_ni$plot_data)
|
||||
|
||||
ggplot(rv_ni$plot_data, aes(x = margin, y = n)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_vline(xintercept = input$delta, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = "Ảnh hưởng của Biên không thua kém (δ) lên Cỡ mẫu",
|
||||
subtitle = "Biên càng lớn (dễ dãi hơn), cỡ mẫu yêu cầu càng nhỏ",
|
||||
x = "Biên không thua kém (δ)",
|
||||
y = "Cỡ mẫu mỗi nhóm (n)"
|
||||
) +
|
||||
annotate("text", x = input$delta * 1.05, y = max(rv_ni$plot_data$n) * 0.9,
|
||||
label = paste0("Biên đã chọn\nδ = ", input$delta),
|
||||
color = "red", hjust = 0) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$ni_params_ui <- renderUI({
|
||||
tagList(
|
||||
h4("Giải thích các tham số chính"),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Biên không thua kém (Non-inferiority Margin - δ)"),
|
||||
p("Đây là tham số ", tags$strong("quan trọng nhất và mang tính chủ quan nhất"), " trong thiết kế nghiên cứu không thua kém. Nó định nghĩa mức độ 'thua kém' tối đa có thể chấp nhận được về mặt lâm sàng. Việc lựa chọn δ phải được biện minh một cách chặt chẽ dựa trên bằng chứng y học và ý kiến chuyên gia, thường là một phần hiệu quả của thuốc chuẩn đã được chứng minh trước đây.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Khác biệt trung bình kỳ vọng (μT - μS)"),
|
||||
p("Đây là sự khác biệt thực sự mà bạn dự đoán giữa hai phương pháp. Trong nhiều trường hợp, để đảm bảo an toàn và tính toán cỡ mẫu đủ lớn, các nhà nghiên cứu thường đặt giá trị này bằng 0 (giả định hai phương pháp có hiệu quả chính xác như nhau).")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Mức ý nghĩa (α, một phía)"),
|
||||
p("Bởi vì giả thuyết của nghiên cứu không thua kém là một chiều (chỉ quan tâm đến việc có 'thua kém' hay không), chúng ta sử dụng mức ý nghĩa một phía. Giá trị α = 0.05 một phía tương ứng với Z-score là 1.645, khác với 1.96 trong kiểm định hai phía.")
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
280
samplesize/reg_linear/app.R
Normal file
280
samplesize/reg_linear/app.R
Normal file
@@ -0,0 +1,280 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO HỒI QUY LOGISTIC (PHIÊN BẢN ỔN ĐỊNH)
|
||||
# - Sửa lỗi hiển thị công thức bằng cách escape ký tự `\` trong R string.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính cỡ mẫu cho hồi quy logistic theo công thức xấp xỉ của Hsieh (1989).
|
||||
calc_n_logistic_hsieh <- function(beta, var_x, p_mean, power = 0.8, sig.level = 0.05) {
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
denom <- (beta^2) * var_x * p_mean * (1 - p_mean)
|
||||
if (denom <= 0) return(NA_real_)
|
||||
n <- (z_a + z_b)^2 / denom
|
||||
ceiling(n)
|
||||
}
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho hồi quy logistic bằng mô phỏng Monte Carlo.
|
||||
simulate_logistic_power <- function(n, beta, intercept = 0, x_dist = list(type = "normal", mean = 0, sd = 1),
|
||||
n_sim = 1000, alpha = 0.05) {
|
||||
sim_results <- replicate(n_sim, {
|
||||
x <- switch(x_dist$type,
|
||||
"normal" = rnorm(n, mean = x_dist$mean, sd = x_dist$sd),
|
||||
"binary" = rbinom(n, size = 1, prob = x_dist$prob),
|
||||
"uniform" = runif(n, min = x_dist$min, max = x_dist$max))
|
||||
|
||||
linpred <- intercept + beta * x
|
||||
p <- 1 / (1 + exp(-linpred))
|
||||
y <- rbinom(n, 1, p)
|
||||
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (inherits(fit, "try-error")) return(NA_real_)
|
||||
|
||||
coefs <- summary(fit)$coefficients
|
||||
if ("x" %in% rownames(coefs)) {
|
||||
as.numeric(coefs["x", "Pr(>|z|)"] < alpha)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Hồi Quy Logistic"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số mô hình"),
|
||||
sliderInput("beta", "Giá trị β (log-odds) kỳ vọng:", value = 0.693, min = -2, max = 2, step = 0.01),
|
||||
sliderInput("p_mean_event", "Tỉ lệ 'event' trung bình trong quần thể:", value = 0.25, min = 0.01, max = 0.99),
|
||||
selectInput("x_type", "Phân phối của biến độc lập X:",
|
||||
choices = c("Nhị phân (0/1)" = "binary", "Chuẩn (Normal)" = "normal", "Đồng nhất (Uniform)" = "uniform")),
|
||||
conditionalPanel("input.x_type == 'normal'", numericInput("x_sd", "Độ lệch chuẩn của X:", value = 1, min = 0.1)),
|
||||
conditionalPanel("input.x_type == 'binary'", sliderInput("x_prob", "Tỉ lệ P(X=1):", value = 0.3, min = 0.01, max = 0.99)),
|
||||
conditionalPanel("input.x_type == 'uniform'", numericInput("x_min", "Min của X:", value = -1), numericInput("x_max", "Max của X:", value = 1)),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định"),
|
||||
sliderInput("power_log", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_log", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
hr(),
|
||||
actionButton("go_log", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "log_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("log_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("log_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail", "Nhập cỡ mẫu (n) để mô phỏng:", value = 200, min = 10)),
|
||||
column(6, actionButton("run_sim_detail", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("log_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("log_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
# --- TAB CÔNG THỨC ĐƯỢC ĐỊNH NGHĨA TĨNH TRONG UI (SỬA LỖI) ---
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
HTML("Kiểm định trong hồi quy logistic thường tập trung vào việc xem hệ số β có khác 0 một cách có ý nghĩa thống kê hay không.<br>"),
|
||||
withMathJax(HTML("$$H_0: \\beta = 0$$")),
|
||||
em("Diễn giải: Biến độc lập X không có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio = 1)."),
|
||||
br(),
|
||||
withMathJax(HTML("$$H_a: \\beta \\neq 0$$")),
|
||||
em("Diễn giải: Biến độc lập X có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio \\neq 1)."),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Xấp xỉ Hsieh, 1989)"),
|
||||
withMathJax(HTML("$$n \\approx \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{\\beta^2 \\cdot \\text{Var}(X) \\cdot \\bar{p}(1-\\bar{p})}$$")),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("\\(Z_{1-\\alpha/2}\\) là giá trị Z-score ứng với mức ý nghĩa \\(\\alpha\\) (ví dụ: 1.96 cho \\(\\alpha=0.05\\).)")),
|
||||
withMathJax(tags$li("\\(Z_{\\text{power}}\\) là giá trị Z-score ứng với công suất mong muốn (ví dụ: 0.84 cho power=0.8).")),
|
||||
withMathJax(tags$li("\\(\\beta\\) là hệ số hồi quy log-odds kỳ vọng (effect size).")),
|
||||
withMathJax(tags$li("\\(\\text{Var}(X)\\) là phương sai của biến độc lập X.")),
|
||||
withMathJax(tags$li("\\(\\bar{p}\\) là tỉ lệ trung bình của kết quả (event) trong quần thể."))
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn tính cỡ mẫu để xem liệu việc ", strong("hút thuốc lá (biến X)"), " có phải là yếu tố nguy cơ cho ", strong("bệnh tăng huyết áp (biến Y)"), " hay không."),
|
||||
p(strong("Diễn giải các tham số:")),
|
||||
tags$ul(
|
||||
tags$li(strong("Kết quả (Event):"), " Bị tăng huyết áp (Y=1)."),
|
||||
tags$li(strong("Biến độc lập (X):"), " Hút thuốc lá (X=1) vs. không hút (X=0). Đây là biến nhị phân."),
|
||||
tags$li(strong("\\(\\beta\\) (Effect size):"), withMathJax(HTML(" Dựa trên y văn, nhà nghiên cứu kỳ vọng Odds Ratio (OR) của việc hút thuốc gây tăng huyết áp là 2.0. Do đó, effect size mong muốn là \\(\\beta = \\ln(OR) = \\ln(2.0) \\approx 0.693\\)"))),
|
||||
tags$li(strong("\\(\\bar{p}\\) (Tỉ lệ event TB):"), withMathJax(HTML(" Tỉ lệ mắc tăng huyết áp chung trong quần thể ước tính là \\(\\bar{p} = 0.25\\)"))),
|
||||
tags$li(strong("\\(\\text{Var}(X)\\):"), withMathJax(HTML(" Tỉ lệ người hút thuốc trong quần thể là 30% (P(X=1)=0.3). Phương sai của X là \\(\\text{Var}(X) = 0.3 \\times 0.7 = 0.21\\)"))),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), withMathJax(HTML(" Nghiên cứu được thiết kế để có 80% cơ hội phát hiện mối liên quan (power=0.8) với mức ý nghĩa 5% (\\(\\alpha=0.05\\))")))
|
||||
),
|
||||
p("Nhà nghiên cứu sẽ nhập các giá trị trên vào công cụ để ước tính cỡ mẫu cần thiết. Các giá trị này cũng đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_log, {
|
||||
varx <- switch(input$x_type,
|
||||
"normal" = input$x_sd^2,
|
||||
"binary" = input$x_prob * (1 - input$x_prob),
|
||||
"uniform" = (input$x_max - input$x_min)^2 / 12)
|
||||
n_hsieh <- calc_n_logistic_hsieh(input$beta, varx, input$p_mean_event, input$power_log, input$alpha_log)
|
||||
rv$log_n_hsieh <- n_hsieh
|
||||
rv$log_varx <- varx
|
||||
|
||||
if (!is.na(n_hsieh)) {
|
||||
n_range_log <- unique(round(seq(max(30, n_hsieh * 0.5), n_hsieh * 1.5, length.out = 15)))
|
||||
xdist_log <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
power_values_log <- map_dbl(n_range_log, ~simulate_logistic_power(n = .x, beta = input$beta, x_dist = xdist_log, n_sim = 500, alpha = input$alpha_log))
|
||||
rv$log_plot_data <- tibble(SampleSize = n_range_log, Power = power_values_log)
|
||||
} else {
|
||||
rv$log_plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$log_result_text <- renderUI({
|
||||
if (input$go_log == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv$log_n_hsieh)
|
||||
n <- rv$log_n_hsieh
|
||||
|
||||
if (is.na(n)) {
|
||||
return(tags$div(class = "alert alert-danger", "Không thể tính toán. Kiểm tra lại các tham số (ví dụ: mẫu số của công thức có thể bằng 0)."))
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Hsieh)"),
|
||||
p("Với Var(X) ≈", tags$b(round(rv$log_varx, 3)), ", để phát hiện một \\(\\beta\\) là", tags$b(input$beta), "với power", tags$b(input$power_log), "và \\(\\alpha\\) là", tags$b(input$alpha_log), ", cỡ mẫu ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", n, " quan sát"),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả xấp xỉ. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn.")
|
||||
)
|
||||
})
|
||||
|
||||
output$log_power_plot <- renderPlot({
|
||||
req(rv$log_plot_data, rv$log_n_hsieh)
|
||||
ggplot(rv$log_plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_log, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv$log_n_hsieh, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Power thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Cỡ mẫu (n)", y = "Power (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv$log_n_hsieh * 1.05, y = 0.1, label = paste("Hsieh Approx.\nn =", rv$log_n_hsieh), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$log_effect_size_ui <- renderUI({
|
||||
or <- exp(input$beta)
|
||||
interpretation <- if (or > 1) {
|
||||
paste0("một sự gia tăng ", round((or - 1) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
} else {
|
||||
paste0("một sự sụt giảm ", round((1-or) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số chênh (Odds Ratio - OR)"),
|
||||
p("Trong hồi quy logistic, effect size thường được biểu diễn bằng Tỷ số chênh (OR), được tính bằng công thức \\(OR = e^\\beta\\)."),
|
||||
p("Nó cho biết odds của một \"event\" (kết quả Y=1) thay đổi như thế nào khi biến độc lập X tăng lên một đơn vị."),
|
||||
hr(),
|
||||
p("Với giá trị \\(\\beta\\) bạn đã chọn là ", tags$b(input$beta), ", Tỷ số chênh tương ứng là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(or, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Khi biến X tăng lên một đơn vị, điều này tương ứng với ", tags$b(interpretation))
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail, {
|
||||
xdist_detail <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
pvals <- replicate(1000, {
|
||||
x <- switch(xdist_detail$type, "normal" = rnorm(input$n_for_sim_detail, 0, xdist_detail$sd), "binary" = rbinom(input$n_for_sim_detail, 1, xdist_detail$prob), "uniform" = runif(input$n_for_sim_detail, xdist_detail$min, xdist_detail$max))
|
||||
p <- 1 / (1 + exp(-(0 + input$beta * x)))
|
||||
y <- rbinom(input$n_for_sim_detail, 1, p)
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (!inherits(fit, "try-error") && "x" %in% rownames(summary(fit)$coefficients)) {
|
||||
summary(fit)$coefficients["x", "Pr(>|z|)"]
|
||||
} else {
|
||||
NA_real_
|
||||
}
|
||||
})
|
||||
rv$log_sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$log_sim_detail_plot <- renderPlot({
|
||||
req(rv$log_sim_detail_data)
|
||||
power_est <- mean(rv$log_sim_detail_data$p_value < input$alpha_log, na.rm=TRUE)
|
||||
|
||||
ggplot(rv$log_sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_log, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (n=", input$n_for_sim_detail, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
278
samplesize/reg_logitstics/app.R
Normal file
278
samplesize/reg_logitstics/app.R
Normal file
@@ -0,0 +1,278 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO HỒI QUY LOGISTIC (PHIÊN BẢN ỔN ĐỊNH)
|
||||
# - Sửa lỗi hiển thị công thức bằng cách escape ký tự `\` trong R string.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính cỡ mẫu cho hồi quy logistic theo công thức xấp xỉ của Hsieh (1989).
|
||||
calc_n_logistic_hsieh <- function(beta, var_x, p_mean, power = 0.8, sig.level = 0.05) {
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
denom <- (beta^2) * var_x * p_mean * (1 - p_mean)
|
||||
if (denom <= 0) return(NA_real_)
|
||||
n <- (z_a + z_b)^2 / denom
|
||||
ceiling(n)
|
||||
}
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho hồi quy logistic bằng mô phỏng Monte Carlo.
|
||||
simulate_logistic_power <- function(n, beta, intercept = 0, x_dist = list(type = "normal", mean = 0, sd = 1),
|
||||
n_sim = 1000, alpha = 0.05) {
|
||||
sim_results <- replicate(n_sim, {
|
||||
x <- switch(x_dist$type,
|
||||
"normal" = rnorm(n, mean = x_dist$mean, sd = x_dist$sd),
|
||||
"binary" = rbinom(n, size = 1, prob = x_dist$prob),
|
||||
"uniform" = runif(n, min = x_dist$min, max = x_dist$max))
|
||||
|
||||
linpred <- intercept + beta * x
|
||||
p <- 1 / (1 + exp(-linpred))
|
||||
y <- rbinom(n, 1, p)
|
||||
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (inherits(fit, "try-error")) return(NA_real_)
|
||||
|
||||
coefs <- summary(fit)$coefficients
|
||||
if ("x" %in% rownames(coefs)) {
|
||||
as.numeric(coefs["x", "Pr(>|z|)"] < alpha)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Hồi Quy Logistic"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số mô hình"),
|
||||
sliderInput("beta", "Giá trị β (log-odds) kỳ vọng:", value = 0.693, min = -2, max = 2, step = 0.01),
|
||||
sliderInput("p_mean_event", "Tỉ lệ 'event' trung bình trong quần thể:", value = 0.25, min = 0.01, max = 0.99),
|
||||
selectInput("x_type", "Phân phối của biến độc lập X:",
|
||||
choices = c("Nhị phân (0/1)" = "binary", "Chuẩn (Normal)" = "normal", "Đồng nhất (Uniform)" = "uniform")),
|
||||
conditionalPanel("input.x_type == 'normal'", numericInput("x_sd", "Độ lệch chuẩn của X:", value = 1, min = 0.1)),
|
||||
conditionalPanel("input.x_type == 'binary'", sliderInput("x_prob", "Tỉ lệ P(X=1):", value = 0.3, min = 0.01, max = 0.99)),
|
||||
conditionalPanel("input.x_type == 'uniform'", numericInput("x_min", "Min của X:", value = -1), numericInput("x_max", "Max của X:", value = 1)),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định"),
|
||||
sliderInput("power_log", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_log", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
hr(),
|
||||
actionButton("go_log", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "log_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("log_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("log_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail", "Nhập cỡ mẫu (n) để mô phỏng:", value = 200, min = 10)),
|
||||
column(6, actionButton("run_sim_detail", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("log_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("log_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
# --- TAB CÔNG THỨC ĐƯỢC ĐỊNH NGHĨA TĨNH TRONG UI (SỬA LỖI) ---
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
p("Kiểm định trong hồi quy logistic thường tập trung vào việc xem hệ số β có khác 0 một cách có ý nghĩa thống kê hay không."),
|
||||
p("$$H_0: \\beta = 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X không có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio = 1).")),
|
||||
p("$$H_a: \\beta \\neq 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio ≠ 1).")),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Xấp xỉ Hsieh, 1989)"),
|
||||
p("$$n \\approx \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{\\beta^2 \\, \\text{Var}(X) \\, \\bar{p}(1-\\bar{p})}$$"),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
tags$li("\\(Z_{1-\\alpha/2}\\) là giá trị Z-score ứng với mức ý nghĩa \\(\\alpha\\) (ví dụ: 1.96 cho \\(\\alpha=0.05\\))."),
|
||||
tags$li("\\(Z_{\\text{power}}\\) là giá trị Z-score ứng với công suất mong muốn (ví dụ: 0.84 cho power=0.8)."),
|
||||
tags$li("\\(\\beta\\) là hệ số hồi quy log-odds kỳ vọng (effect size)."),
|
||||
tags$li("\\(\\text{Var}(X)\\) là phương sai của biến độc lập X."),
|
||||
tags$li("\\(\\bar{p}\\) là tỉ lệ trung bình của kết quả (event) trong quần thể.")
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn tính cỡ mẫu để xem liệu việc ", strong("hút thuốc lá (biến X)"), " có phải là yếu tố nguy cơ cho ", strong("bệnh tăng huyết áp (biến Y)"), " hay không."),
|
||||
p(strong("Diễn giải các tham số:")),
|
||||
tags$ul(
|
||||
tags$li(strong("Kết quả (Event):"), " Bị tăng huyết áp (Y=1)."),
|
||||
tags$li(strong("Biến độc lập (X):"), " Hút thuốc lá (X=1) vs. không hút (X=0). Đây là biến nhị phân."),
|
||||
tags$li(strong("\\(\\beta\\) (Effect size):"), " Dựa trên y văn, nhà nghiên cứu kỳ vọng Odds Ratio (OR) của việc hút thuốc gây tăng huyết áp là 2.0. Do đó, effect size mong muốn là \\(\\beta = \\ln(OR) = \\ln(2.0) \\approx 0.693\\)."),
|
||||
tags$li(strong("\\(\\bar{p}\\) (Tỉ lệ event TB):"), " Tỉ lệ mắc tăng huyết áp chung trong quần thể ước tính là 25% (\\(\\bar{p} = 0.25\\))."),
|
||||
tags$li(strong("\\(\\text{Var}(X)\\):"), " Tỉ lệ người hút thuốc trong quần thể là 30% (P(X=1)=0.3). Phương sai của X là \\(\\text{Var}(X) = P(X=1) \\times (1-P(X=1)) = 0.3 \\times 0.7 = 0.21\\)."),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), " Nghiên cứu được thiết kế để có 80% cơ hội phát hiện mối liên quan (power=0.8) với mức ý nghĩa 5% (\\(\\alpha=0.05\\)).")
|
||||
),
|
||||
p("Nhà nghiên cứu sẽ nhập các giá trị trên vào công cụ để ước tính cỡ mẫu cần thiết. Các giá trị này cũng đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_log, {
|
||||
varx <- switch(input$x_type,
|
||||
"normal" = input$x_sd^2,
|
||||
"binary" = input$x_prob * (1 - input$x_prob),
|
||||
"uniform" = (input$x_max - input$x_min)^2 / 12)
|
||||
n_hsieh <- calc_n_logistic_hsieh(input$beta, varx, input$p_mean_event, input$power_log, input$alpha_log)
|
||||
rv$log_n_hsieh <- n_hsieh
|
||||
rv$log_varx <- varx
|
||||
|
||||
if (!is.na(n_hsieh)) {
|
||||
n_range_log <- unique(round(seq(max(30, n_hsieh * 0.5), n_hsieh * 1.5, length.out = 15)))
|
||||
xdist_log <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
power_values_log <- map_dbl(n_range_log, ~simulate_logistic_power(n = .x, beta = input$beta, x_dist = xdist_log, n_sim = 500, alpha = input$alpha_log))
|
||||
rv$log_plot_data <- tibble(SampleSize = n_range_log, Power = power_values_log)
|
||||
} else {
|
||||
rv$log_plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$log_result_text <- renderUI({
|
||||
if (input$go_log == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv$log_n_hsieh)
|
||||
n <- rv$log_n_hsieh
|
||||
|
||||
if (is.na(n)) {
|
||||
return(tags$div(class = "alert alert-danger", "Không thể tính toán. Kiểm tra lại các tham số (ví dụ: mẫu số của công thức có thể bằng 0)."))
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Hsieh)"),
|
||||
p("Với Var(X) ≈", tags$b(round(rv$log_varx, 3)), ", để phát hiện một \\(\\beta\\) là", tags$b(input$beta), "với power", tags$b(input$power_log), "và \\(\\alpha\\) là", tags$b(input$alpha_log), ", cỡ mẫu ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", n, " quan sát"),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả xấp xỉ. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn.")
|
||||
)
|
||||
})
|
||||
|
||||
output$log_power_plot <- renderPlot({
|
||||
req(rv$log_plot_data, rv$log_n_hsieh)
|
||||
ggplot(rv$log_plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_log, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv$log_n_hsieh, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Power thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Cỡ mẫu (n)", y = "Power (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv$log_n_hsieh * 1.05, y = 0.1, label = paste("Hsieh Approx.\nn =", rv$log_n_hsieh), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$log_effect_size_ui <- renderUI({
|
||||
or <- exp(input$beta)
|
||||
interpretation <- if (or > 1) {
|
||||
paste0("một sự gia tăng ", round((or - 1) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
} else {
|
||||
paste0("một sự sụt giảm ", round((1-or) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số chênh (Odds Ratio - OR)"),
|
||||
p("Trong hồi quy logistic, effect size thường được biểu diễn bằng Tỷ số chênh (OR), được tính bằng công thức \\(OR = e^\\beta\\)."),
|
||||
p("Nó cho biết odds của một \"event\" (kết quả Y=1) thay đổi như thế nào khi biến độc lập X tăng lên một đơn vị."),
|
||||
hr(),
|
||||
p("Với giá trị \\(\\beta\\) bạn đã chọn là ", tags$b(input$beta), ", Tỷ số chênh tương ứng là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(or, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Khi biến X tăng lên một đơn vị, điều này tương ứng với ", tags$b(interpretation))
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail, {
|
||||
xdist_detail <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
pvals <- replicate(1000, {
|
||||
x <- switch(xdist_detail$type, "normal" = rnorm(input$n_for_sim_detail, 0, xdist_detail$sd), "binary" = rbinom(input$n_for_sim_detail, 1, xdist_detail$prob), "uniform" = runif(input$n_for_sim_detail, xdist_detail$min, xdist_detail$max))
|
||||
p <- 1 / (1 + exp(-(0 + input$beta * x)))
|
||||
y <- rbinom(input$n_for_sim_detail, 1, p)
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (!inherits(fit, "try-error") && "x" %in% rownames(summary(fit)$coefficients)) {
|
||||
summary(fit)$coefficients["x", "Pr(>|z|)"]
|
||||
} else {
|
||||
NA_real_
|
||||
}
|
||||
})
|
||||
rv$log_sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$log_sim_detail_plot <- renderPlot({
|
||||
req(rv$log_sim_detail_data)
|
||||
power_est <- mean(rv$log_sim_detail_data$p_value < input$alpha_log, na.rm=TRUE)
|
||||
|
||||
ggplot(rv$log_sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_log, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (n=", input$n_for_sim_detail, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
278
samplesize/reg_logitstics/app.R1
Normal file
278
samplesize/reg_logitstics/app.R1
Normal file
@@ -0,0 +1,278 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO HỒI QUY LOGISTIC (PHIÊN BẢN ỔN ĐỊNH)
|
||||
# - Sửa lỗi hiển thị công thức bằng cách escape ký tự `\` trong R string.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính cỡ mẫu cho hồi quy logistic theo công thức xấp xỉ của Hsieh (1989).
|
||||
calc_n_logistic_hsieh <- function(beta, var_x, p_mean, power = 0.8, sig.level = 0.05) {
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
denom <- (beta^2) * var_x * p_mean * (1 - p_mean)
|
||||
if (denom <= 0) return(NA_real_)
|
||||
n <- (z_a + z_b)^2 / denom
|
||||
ceiling(n)
|
||||
}
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho hồi quy logistic bằng mô phỏng Monte Carlo.
|
||||
simulate_logistic_power <- function(n, beta, intercept = 0, x_dist = list(type = "normal", mean = 0, sd = 1),
|
||||
n_sim = 1000, alpha = 0.05) {
|
||||
sim_results <- replicate(n_sim, {
|
||||
x <- switch(x_dist$type,
|
||||
"normal" = rnorm(n, mean = x_dist$mean, sd = x_dist$sd),
|
||||
"binary" = rbinom(n, size = 1, prob = x_dist$prob),
|
||||
"uniform" = runif(n, min = x_dist$min, max = x_dist$max))
|
||||
|
||||
linpred <- intercept + beta * x
|
||||
p <- 1 / (1 + exp(-linpred))
|
||||
y <- rbinom(n, 1, p)
|
||||
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (inherits(fit, "try-error")) return(NA_real_)
|
||||
|
||||
coefs <- summary(fit)$coefficients
|
||||
if ("x" %in% rownames(coefs)) {
|
||||
as.numeric(coefs["x", "Pr(>|z|)"] < alpha)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Hồi Quy Logistic"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số mô hình"),
|
||||
sliderInput("beta", "Giá trị β (log-odds) kỳ vọng:", value = 0.693, min = -2, max = 2, step = 0.01),
|
||||
sliderInput("p_mean_event", "Tỉ lệ 'event' trung bình trong quần thể:", value = 0.25, min = 0.01, max = 0.99),
|
||||
selectInput("x_type", "Phân phối của biến độc lập X:",
|
||||
choices = c("Nhị phân (0/1)" = "binary", "Chuẩn (Normal)" = "normal", "Đồng nhất (Uniform)" = "uniform")),
|
||||
conditionalPanel("input.x_type == 'normal'", numericInput("x_sd", "Độ lệch chuẩn của X:", value = 1, min = 0.1)),
|
||||
conditionalPanel("input.x_type == 'binary'", sliderInput("x_prob", "Tỉ lệ P(X=1):", value = 0.3, min = 0.01, max = 0.99)),
|
||||
conditionalPanel("input.x_type == 'uniform'", numericInput("x_min", "Min của X:", value = -1), numericInput("x_max", "Max của X:", value = 1)),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định"),
|
||||
sliderInput("power_log", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_log", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
hr(),
|
||||
actionButton("go_log", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "log_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("log_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("log_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail", "Nhập cỡ mẫu (n) để mô phỏng:", value = 200, min = 10)),
|
||||
column(6, actionButton("run_sim_detail", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("log_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("log_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
# --- TAB CÔNG THỨC ĐƯỢC ĐỊNH NGHĨA TĨNH TRONG UI (SỬA LỖI) ---
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
p("Kiểm định trong hồi quy logistic thường tập trung vào việc xem hệ số β có khác 0 một cách có ý nghĩa thống kê hay không."),
|
||||
p("$$H_0: \\beta = 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X không có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio = 1).")),
|
||||
p("$$H_a: \\beta \\neq 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio ≠ 1).")),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Xấp xỉ Hsieh, 1989)"),
|
||||
p("$$n \\approx \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{\\beta^2 \\, \\text{Var}(X) \\, \\bar{p}(1-\\bar{p})}$$"),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
tags$li("\\(Z_{1-\\alpha/2}\\) là giá trị Z-score ứng với mức ý nghĩa \\(\\alpha\\) (ví dụ: 1.96 cho \\(\\alpha=0.05\\))."),
|
||||
tags$li("\\(Z_{\\text{power}}\\) là giá trị Z-score ứng với công suất mong muốn (ví dụ: 0.84 cho power=0.8)."),
|
||||
tags$li("\\(\\beta\\) là hệ số hồi quy log-odds kỳ vọng (effect size)."),
|
||||
tags$li("\\(\\text{Var}(X)\\) là phương sai của biến độc lập X."),
|
||||
tags$li("\\(\\bar{p}\\) là tỉ lệ trung bình của kết quả (event) trong quần thể.")
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn tính cỡ mẫu để xem liệu việc ", strong("hút thuốc lá (biến X)"), " có phải là yếu tố nguy cơ cho ", strong("bệnh tăng huyết áp (biến Y)"), " hay không."),
|
||||
p(strong("Diễn giải các tham số:")),
|
||||
tags$ul(
|
||||
tags$li(strong("Kết quả (Event):"), " Bị tăng huyết áp (Y=1)."),
|
||||
tags$li(strong("Biến độc lập (X):"), " Hút thuốc lá (X=1) vs. không hút (X=0). Đây là biến nhị phân."),
|
||||
tags$li(strong("\\(\\beta\\) (Effect size):"), " Dựa trên y văn, nhà nghiên cứu kỳ vọng Odds Ratio (OR) của việc hút thuốc gây tăng huyết áp là 2.0. Do đó, effect size mong muốn là \\(\\beta = \\ln(OR) = \\ln(2.0) \\approx 0.693\\)."),
|
||||
tags$li(strong("\\(\\bar{p}\\) (Tỉ lệ event TB):"), " Tỉ lệ mắc tăng huyết áp chung trong quần thể ước tính là 25% (\\(\\bar{p} = 0.25\\))."),
|
||||
tags$li(strong("\\(\\text{Var}(X)\\):"), " Tỉ lệ người hút thuốc trong quần thể là 30% (P(X=1)=0.3). Phương sai của X là \\(\\text{Var}(X) = P(X=1) \\times (1-P(X=1)) = 0.3 \\times 0.7 = 0.21\\)."),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), " Nghiên cứu được thiết kế để có 80% cơ hội phát hiện mối liên quan (power=0.8) với mức ý nghĩa 5% (\\(\\alpha=0.05\\)).")
|
||||
),
|
||||
p("Nhà nghiên cứu sẽ nhập các giá trị trên vào công cụ để ước tính cỡ mẫu cần thiết. Các giá trị này cũng đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_log, {
|
||||
varx <- switch(input$x_type,
|
||||
"normal" = input$x_sd^2,
|
||||
"binary" = input$x_prob * (1 - input$x_prob),
|
||||
"uniform" = (input$x_max - input$x_min)^2 / 12)
|
||||
n_hsieh <- calc_n_logistic_hsieh(input$beta, varx, input$p_mean_event, input$power_log, input$alpha_log)
|
||||
rv$log_n_hsieh <- n_hsieh
|
||||
rv$log_varx <- varx
|
||||
|
||||
if (!is.na(n_hsieh)) {
|
||||
n_range_log <- unique(round(seq(max(30, n_hsieh * 0.5), n_hsieh * 1.5, length.out = 15)))
|
||||
xdist_log <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
power_values_log <- map_dbl(n_range_log, ~simulate_logistic_power(n = .x, beta = input$beta, x_dist = xdist_log, n_sim = 500, alpha = input$alpha_log))
|
||||
rv$log_plot_data <- tibble(SampleSize = n_range_log, Power = power_values_log)
|
||||
} else {
|
||||
rv$log_plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$log_result_text <- renderUI({
|
||||
if (input$go_log == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv$log_n_hsieh)
|
||||
n <- rv$log_n_hsieh
|
||||
|
||||
if (is.na(n)) {
|
||||
return(tags$div(class = "alert alert-danger", "Không thể tính toán. Kiểm tra lại các tham số (ví dụ: mẫu số của công thức có thể bằng 0)."))
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Hsieh)"),
|
||||
p("Với Var(X) ≈", tags$b(round(rv$log_varx, 3)), ", để phát hiện một \\(\\beta\\) là", tags$b(input$beta), "với power", tags$b(input$power_log), "và \\(\\alpha\\) là", tags$b(input$alpha_log), ", cỡ mẫu ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", n, " quan sát"),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả xấp xỉ. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn.")
|
||||
)
|
||||
})
|
||||
|
||||
output$log_power_plot <- renderPlot({
|
||||
req(rv$log_plot_data, rv$log_n_hsieh)
|
||||
ggplot(rv$log_plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_log, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv$log_n_hsieh, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Power thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Cỡ mẫu (n)", y = "Power (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv$log_n_hsieh * 1.05, y = 0.1, label = paste("Hsieh Approx.\nn =", rv$log_n_hsieh), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$log_effect_size_ui <- renderUI({
|
||||
or <- exp(input$beta)
|
||||
interpretation <- if (or > 1) {
|
||||
paste0("một sự gia tăng ", round((or - 1) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
} else {
|
||||
paste0("một sự sụt giảm ", round((1-or) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số chênh (Odds Ratio - OR)"),
|
||||
p("Trong hồi quy logistic, effect size thường được biểu diễn bằng Tỷ số chênh (OR), được tính bằng công thức \\(OR = e^\\beta\\)."),
|
||||
p("Nó cho biết odds của một \"event\" (kết quả Y=1) thay đổi như thế nào khi biến độc lập X tăng lên một đơn vị."),
|
||||
hr(),
|
||||
p("Với giá trị \\(\\beta\\) bạn đã chọn là ", tags$b(input$beta), ", Tỷ số chênh tương ứng là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(or, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Khi biến X tăng lên một đơn vị, điều này tương ứng với ", tags$b(interpretation))
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail, {
|
||||
xdist_detail <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
pvals <- replicate(1000, {
|
||||
x <- switch(xdist_detail$type, "normal" = rnorm(input$n_for_sim_detail, 0, xdist_detail$sd), "binary" = rbinom(input$n_for_sim_detail, 1, xdist_detail$prob), "uniform" = runif(input$n_for_sim_detail, xdist_detail$min, xdist_detail$max))
|
||||
p <- 1 / (1 + exp(-(0 + input$beta * x)))
|
||||
y <- rbinom(input$n_for_sim_detail, 1, p)
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (!inherits(fit, "try-error") && "x" %in% rownames(summary(fit)$coefficients)) {
|
||||
summary(fit)$coefficients["x", "Pr(>|z|)"]
|
||||
} else {
|
||||
NA_real_
|
||||
}
|
||||
})
|
||||
rv$log_sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$log_sim_detail_plot <- renderPlot({
|
||||
req(rv$log_sim_detail_data)
|
||||
power_est <- mean(rv$log_sim_detail_data$p_value < input$alpha_log, na.rm=TRUE)
|
||||
|
||||
ggplot(rv$log_sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_log, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (n=", input$n_for_sim_detail, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
354
samplesize/survival/app.R
Normal file
354
samplesize/survival/app.R
Normal file
@@ -0,0 +1,354 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO PHÂN TÍCH SỐNG CÒN (LOG-RANK TEST)
|
||||
# - Xây dựng dựa trên cấu trúc ứng dụng Hồi quy Logistic.
|
||||
# - Bao gồm tính toán, mô phỏng, và diễn giải chi tiết.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
library(survival)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính số biến cố (events) cần thiết cho kiểm định Log-rank (Schoenfeld, 1983).
|
||||
#'
|
||||
#' @param hr Tỷ số rủi ro (Hazard Ratio) kỳ vọng.
|
||||
#' @param power Công suất mong muốn (1 - beta).
|
||||
#' @param sig.level Mức ý nghĩa alpha.
|
||||
#' @param p_alloc Tỷ lệ phân bổ vào nhóm 1 (can thiệp).
|
||||
#' @return Số nguyên là tổng số biến cố cần thiết.
|
||||
calc_events_logrank <- function(hr, power = 0.8, sig.level = 0.05, p_alloc = 0.5) {
|
||||
if (hr <= 0) return(NA_real_)
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
|
||||
num <- (z_a + z_b)^2
|
||||
den <- (log(hr)^2) * p_alloc * (1 - p_alloc)
|
||||
|
||||
if (den <= 0) return(NA_real_)
|
||||
|
||||
ceiling(num / den)
|
||||
}
|
||||
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho kiểm định Log-rank bằng mô phỏng Monte Carlo.
|
||||
#'
|
||||
#' @param n Tổng cỡ mẫu.
|
||||
#' @param hr Tỷ số rủi ro.
|
||||
#' @param surv_prob_control Tỷ lệ sống sót ở nhóm chứng sau một thời gian theo dõi.
|
||||
#' @param follow_up_time Thời gian theo dõi.
|
||||
#' @param alpha Mức ý nghĩa.
|
||||
#' @param p_alloc Tỷ lệ phân bổ vào nhóm 1.
|
||||
#' @param n_sim Số lần mô phỏng.
|
||||
#' @return Giá trị công suất thực nghiệm (từ 0 đến 1).
|
||||
simulate_logrank_power <- function(n, hr, surv_prob_control, follow_up_time = 1,
|
||||
alpha = 0.05, p_alloc = 0.5, n_sim = 500) {
|
||||
|
||||
# Từ tỷ lệ sống, tính rate lambda cho phân phối mũ
|
||||
rate_control <- -log(surv_prob_control) / follow_up_time
|
||||
rate_treatment <- rate_control * hr
|
||||
|
||||
n1 <- round(n * p_alloc)
|
||||
n2 <- n - n1
|
||||
|
||||
sim_results <- replicate(n_sim, {
|
||||
# Tạo dữ liệu thời gian sống cho 2 nhóm
|
||||
times1 <- rexp(n1, rate = rate_treatment)
|
||||
times2 <- rexp(n2, rate = rate_control)
|
||||
|
||||
# Tạo dữ liệu kiểm duyệt (censoring) tại thời điểm follow_up_time
|
||||
event1 <- ifelse(times1 < follow_up_time, 1, 0)
|
||||
event2 <- ifelse(times2 < follow_up_time, 1, 0)
|
||||
|
||||
time_obs1 <- pmin(times1, follow_up_time)
|
||||
time_obs2 <- pmin(times2, follow_up_time)
|
||||
|
||||
# Gộp dữ liệu
|
||||
all_times <- c(time_obs1, time_obs2)
|
||||
all_events <- c(event1, event2)
|
||||
group <- factor(c(rep("Treatment", n1), rep("Control", n2)))
|
||||
|
||||
# Thực hiện kiểm định log-rank
|
||||
sdf <- try(survdiff(Surv(all_times, all_events) ~ group), silent = TRUE)
|
||||
|
||||
if (inherits(sdf, "try-error")) return(NA)
|
||||
|
||||
# Lấy p-value
|
||||
p_val <- 1 - pchisq(sdf$chisq, df = 1)
|
||||
return(p_val < alpha)
|
||||
})
|
||||
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Phân Tích Sống Còn (Log-Rank)"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số hiệu ứng & Quần thể"),
|
||||
sliderInput("s_a", "Tỷ lệ sống còn kỳ vọng ở Nhóm 1 (Can thiệp):", value = 0.45, min = 0.01, max = 0.99),
|
||||
sliderInput("s_b", "Tỷ lệ sống còn kỳ vọng ở Nhóm 2 (Chứng):", value = 0.30, min = 0.01, max = 0.99),
|
||||
numericInput("follow_up", "Thời gian theo dõi (đơn vị tùy chọn, ví dụ: năm):", value=3, min=0.1),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định & Thiết kế"),
|
||||
sliderInput("power_surv", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_surv", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
sliderInput("p_alloc_surv", "Tỷ lệ phân bổ vào Nhóm 1:", value = 0.5, min = 0.1, max = 0.9),
|
||||
helpText("Giá trị 0.5 tương ứng tỷ lệ 1:1."),
|
||||
|
||||
hr(),
|
||||
actionButton("go_surv", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "surv_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("surv_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("surv_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail_surv", "Nhập cỡ mẫu (N) để mô phỏng:", value = 300, min = 20)),
|
||||
column(6, actionButton("run_sim_detail_surv", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("surv_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("surv_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
HTML("Kiểm định Log-rank so sánh toàn bộ đường cong sống còn giữa các nhóm. Giả thuyết không có tham số cụ thể như hồi quy, mà là:"),
|
||||
p(strong("$$H_0: S_1(t) = S_2(t) \\text{ for all } t$$")),
|
||||
em("Diễn giải: Không có sự khác biệt về hàm sống còn giữa nhóm 1 và nhóm 2 tại mọi thời điểm."),
|
||||
br(),
|
||||
p(strong("$$H_a: S_1(t) \\neq S_2(t) \\text{ for some } t$$")),
|
||||
em("Diễn giải: Có sự khác biệt về hàm sống còn giữa hai nhóm."),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Schoenfeld, 1983)"),
|
||||
p("Quá trình tính toán gồm 2 bước:"),
|
||||
p(strong("Bước 1: Tính tổng số biến cố (events) cần thiết (d)")),
|
||||
withMathJax(HTML("$$d = \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{(\\ln(HR))^2 \\cdot p_1 \\cdot p_2}$$")),
|
||||
p(strong("Bước 2: Tính tổng cỡ mẫu (N) từ số biến cố")),
|
||||
withMathJax(HTML("$$N = \\frac{d}{P(\\text{event})}$$")),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("\\(HR\\) là Tỷ số rủi ro (Hazard Ratio) kỳ vọng. Có thể ước tính \\(HR \\approx \\frac{\\ln(S_1)}{\\ln(S_2)}\\).")),
|
||||
withMathJax(tags$li("\\(p_1, p_2\\) là tỷ lệ phân bổ vào mỗi nhóm (ví dụ: 0.5 và 0.5).")),
|
||||
withMathJax(tags$li("\\(P(\\text{event})\\) là xác suất trung bình một người tham gia sẽ có biến cố trong thời gian nghiên cứu. \\(P(\\text{event}) = 1 - (p_1 S_1 + p_2 S_2)\\)."))
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn thử nghiệm một loại thuốc mới (Nhóm 1) để kéo dài thời gian sống không tái phát của bệnh nhân ung thư so với phác đồ chuẩn (Nhóm 2). Thời gian theo dõi là 3 năm."),
|
||||
tags$ul(
|
||||
tags$li(strong("Tỷ lệ sống còn Nhóm 2 (chuẩn):"), withMathJax(HTML(" Dựa trên dữ liệu trước, tỷ lệ sống không tái phát sau 3 năm của phác đồ chuẩn là 30% (\\(S_2=0.3\\))."))),
|
||||
tags$li(strong("Tỷ lệ sống còn Nhóm 1 (mới):"), withMathJax(HTML(" Nhà nghiên cứu kỳ vọng thuốc mới sẽ cải thiện tỷ lệ này lên 45% (\\(S_1=0.45\\))."))),
|
||||
tags$li(strong("Effect size (HR):"), withMathJax(HTML(" HR ước tính là \\(\\frac{\\ln(0.45)}{\\ln(0.30)} \\approx 0.66\\)."))),
|
||||
tags$li(strong("Xác suất biến cố (tái phát):"), withMathJax(HTML(" \\(P(\\text{event}) = 1 - (0.5 \\times 0.45 + 0.5 \\times 0.30) = 0.625\\)."))),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), " Nghiên cứu cần 80% công suất và mức ý nghĩa 5%."),
|
||||
tags$li(strong("Thiết kế:"), " Phân bổ đều bệnh nhân vào 2 nhóm (1:1).")
|
||||
),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_surv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_surv, {
|
||||
validate(
|
||||
need(input$s_a != input$s_b, "Tỷ lệ sống còn ở hai nhóm phải khác nhau.")
|
||||
)
|
||||
|
||||
# Tính toán
|
||||
hr_est <- log(input$s_a) / log(input$s_b)
|
||||
d_req <- calc_events_logrank(hr_est, input$power_surv, input$alpha_surv, input$p_alloc_surv)
|
||||
|
||||
prob_event_1 <- 1 - input$s_a
|
||||
prob_event_2 <- 1 - input$s_b
|
||||
avg_prob_event <- input$p_alloc_surv * prob_event_1 + (1 - input$p_alloc_surv) * prob_event_2
|
||||
|
||||
n_total <- ceiling(d_req / avg_prob_event)
|
||||
n1 <- ceiling(n_total * input$p_alloc_surv)
|
||||
n2 <- n_total - n1
|
||||
|
||||
rv_surv$hr <- hr_est
|
||||
rv_surv$d <- d_req
|
||||
rv_surv$N <- n_total
|
||||
rv_surv$n1 <- n1
|
||||
rv_surv$n2 <- n2
|
||||
|
||||
# Dữ liệu cho biểu đồ
|
||||
if (!is.na(n_total)) {
|
||||
n_range_surv <- unique(round(seq(max(30, n_total * 0.5), n_total * 1.5, length.out = 15)))
|
||||
|
||||
power_values_surv <- map_dbl(n_range_surv, ~simulate_logrank_power(
|
||||
n = .x,
|
||||
hr = hr_est,
|
||||
surv_prob_control = input$s_b,
|
||||
follow_up_time = input$follow_up,
|
||||
alpha = input$alpha_surv,
|
||||
p_alloc = input$p_alloc_surv,
|
||||
n_sim = 500
|
||||
))
|
||||
rv_surv$plot_data <- tibble(SampleSize = n_range_surv, Power = power_values_surv)
|
||||
} else {
|
||||
rv_surv$plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$surv_result_text <- renderUI({
|
||||
if (input$go_surv == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_surv$N)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Schoenfeld)"),
|
||||
p("Với Hazard Ratio (HR) ước tính là", tags$b(round(rv_surv$hr, 3)), "và các tham số đã cho, kết quả như sau:"),
|
||||
tags$div(class="alert alert-light", style="text-align: center;",
|
||||
p(style="font-size: 1.2em;", "Tổng số biến cố (events) cần quan sát: ", tags$strong(style="color: #dc3545;", rv_surv$d))
|
||||
),
|
||||
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", "Tổng Cỡ Mẫu (N) ≈ ", rv_surv$N),
|
||||
p(style = "text-align: center; font-size: 1.2em;",
|
||||
"Cỡ mẫu Nhóm 1 (Can thiệp): ", tags$b(rv_surv$n1), br(),
|
||||
"Cỡ mẫu Nhóm 2 (Chứng): ", tags$b(rv_surv$n2)
|
||||
),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả dựa trên công thức. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn, đặc biệt với cỡ mẫu nhỏ.")
|
||||
)
|
||||
})
|
||||
|
||||
output$surv_power_plot <- renderPlot({
|
||||
req(rv_surv$plot_data, rv_surv$N)
|
||||
ggplot(rv_surv$plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_surv, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv_surv$N, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Công suất thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Tổng Cỡ mẫu (N)", y = "Công suất (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv_surv$N * 1.05, y = 0.1, label = paste("Formula Approx.\nN =", rv_surv$N), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$surv_effect_size_ui <- renderUI({
|
||||
req(rv_surv$hr)
|
||||
hr <- rv_surv$hr
|
||||
|
||||
interpretation <- if (hr < 1) {
|
||||
paste0("rủi ro (hazard) xảy ra biến cố tại bất kỳ thời điểm nào ", tags$b("giảm đi ", round((1 - hr) * 100, 1), "%"), " so với nhóm chứng.")
|
||||
} else {
|
||||
paste0("rủi ro (hazard) xảy ra biến cố tại bất kỳ thời điểm nào ", tags$b("tăng lên ", round((hr - 1) * 100, 1), "%"), " so với nhóm chứng.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số rủi ro (Hazard Ratio - HR)"),
|
||||
p("Trong phân tích sống còn, effect size chính là Tỷ số rủi ro (HR). Nó đo lường hiệu quả tức thời của một can thiệp lên xác suất xảy ra biến cố."),
|
||||
p("Nó được ước tính từ tỷ lệ sống còn của hai nhóm (giả định rằng tỷ lệ rủi ro là hằng số theo thời gian): \\(HR \\approx \\ln(S_1) / \\ln(S_2)\\)."),
|
||||
hr(),
|
||||
p("Với các tỷ lệ sống còn bạn đã chọn, Tỷ số rủi ro ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(hr, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Một HR bằng ", tags$b(round(hr, 3)), " có nghĩa là nhóm can thiệp có ", interpretation)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail_surv, {
|
||||
pvals <- replicate(1000, {
|
||||
n <- input$n_for_sim_detail_surv
|
||||
hr <- log(input$s_a) / log(input$s_b)
|
||||
|
||||
rate_control <- -log(input$s_b) / input$follow_up
|
||||
rate_treatment <- rate_control * hr
|
||||
|
||||
n1 <- round(n * input$p_alloc_surv)
|
||||
n2 <- n - n1
|
||||
|
||||
times1 <- rexp(n1, rate = rate_treatment)
|
||||
times2 <- rexp(n2, rate = rate_control)
|
||||
|
||||
event1 <- ifelse(times1 < input$follow_up, 1, 0)
|
||||
event2 <- ifelse(times2 < input$follow_up, 1, 0)
|
||||
|
||||
time_obs1 <- pmin(times1, input$follow_up)
|
||||
time_obs2 <- pmin(times2, input$follow_up)
|
||||
|
||||
all_times <- c(time_obs1, time_obs2)
|
||||
all_events <- c(event1, event2)
|
||||
group <- factor(c(rep("Treatment", n1), rep("Control", n2)))
|
||||
|
||||
sdf <- try(survdiff(Surv(all_times, all_events) ~ group), silent = TRUE)
|
||||
if (inherits(sdf, "try-error")) return(NA_real_)
|
||||
|
||||
1 - pchisq(sdf$chisq, df = 1)
|
||||
})
|
||||
rv_surv$sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$surv_sim_detail_plot <- renderPlot({
|
||||
req(rv_surv$sim_detail_data)
|
||||
power_est <- mean(rv_surv$sim_detail_data$p_value < input$alpha_surv, na.rm=TRUE)
|
||||
|
||||
ggplot(rv_surv$sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_surv, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (N=", input$n_for_sim_detail_surv, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value (từ Kiểm định Log-rank)", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
Reference in New Issue
Block a user