Nothing
#' Tile Plot of Item Response Distributions
#'
#' Creates a tile (heat map) plot showing the distribution of
#' responses across all items and response categories. Each cell
#' displays the count (or percentage) of responses, with optional
#' conditional highlighting for cells with low counts. This is a
#' descriptive data visualization tool intended for use before
#' model fitting.
#'
#' @param data A data frame in wide format containing only the item
#' response columns. Each column is one item, each row is one
#' person. All columns must be numeric (integer-valued). Response
#' categories may be coded starting from 0 or 1. Do not include
#' person IDs, grouping variables, or other non-item columns.
#' @param cutoff Integer. Cells with counts below this value are
#' highlighted (when \code{highlight = TRUE}). Default is 10.
#' @param highlight Logical. If \code{TRUE} (the default), cell
#' labels with counts below \code{cutoff} are displayed in red.
#' This includes cells with zero responses (empty categories),
#' which is useful for identifying gaps in the response
#' distribution.
#' @param percent Logical. If \code{TRUE}, cell labels show
#' percentages instead of raw counts. Default is \code{FALSE}.
#' @param text_color Character. Color for cell label text (when
#' not highlighted). Default is \code{"orange"}.
#' @param item_labels An optional character vector of descriptive
#' labels for the items (y-axis). Must be the same length as
#' \code{ncol(data)}. If \code{NULL} (the default), column names
#' are used.
#' @param category_labels An optional character vector of labels
#' for the response categories (x-axis). Must be the same length
#' as the number of response categories spanning from the minimum
#' to the maximum observed value. If \code{NULL} (the default),
#' numeric category values are used.
#'
#' @return A \code{\link[ggplot2]{ggplot}} object.
#'
#' @details
#' The plot displays items on the y-axis (in the same order as the
#' columns in \code{data}, from top to bottom) and response
#' categories on the x-axis. Cell shading represents the count
#' of responses (darker = more responses). Cell labels show either
#' raw counts or percentages.
#'
#' Categories with zero responses are explicitly shown (as cells
#' with \code{n = 0}), which helps identify gaps in the response
#' distribution — one of the primary purposes of this plot.
#'
#' \strong{Input requirements:}
#' \itemize{
#' \item All columns must be numeric (integer-valued).
#' \item The data frame must contain at least 2 columns (items)
#' and at least 1 row (person).
#' }
#'
#' @examples
#' library(ggplot2)
#' if (requireNamespace("eRm", quietly = TRUE))
#'
#' # Basic tile plot
#' plot_tile(eRm::pcmdat2)
#'
#' # With custom item labels
#' plot_tile(
#' eRm::pcmdat2,
#' item_labels = c("Mood", "Sleep", "Appetite", "Energy")
#' )
#'
#' # With custom category labels and percentages
#' plot_tile(
#' eRm::pcmdat2,
#' category_labels = c("Never", "Sometimes", "Often"),
#' percent = TRUE
#' )
#'
#' # Adjust cutoff for highlighting
#' plot_tile(eRm::pcmdat2, cutoff = 20, highlight = TRUE)
#'
#' @importFrom rlang .data
#' @export
plot_tile <- function(
data,
cutoff = 10,
highlight = TRUE,
percent = FALSE,
text_color = "orange",
item_labels = NULL,
category_labels = NULL
) {
if (!requireNamespace("ggplot2", quietly = TRUE)) {
stop("Package 'ggplot2' is required.", call. = FALSE)
}
# ================================================================
# INPUT VALIDATION
# ================================================================
if (!is.data.frame(data)) {
stop("'data' must be a data frame in wide format ",
"(one column per item, one row per person).",
call. = FALSE)
}
if (ncol(data) < 2) {
stop("'data' must contain at least 2 columns (items). ",
"Found ", ncol(data), " column(s).", call. = FALSE)
}
if (nrow(data) < 1) {
stop("'data' must contain at least 1 row (person). ",
"Found 0 rows.", call. = FALSE)
}
# Check all columns are numeric
non_numeric <- names(data)[!vapply(data, is.numeric, logical(1))]
if (length(non_numeric) > 0) {
stop("All columns must be numeric. Non-numeric column(s): ",
paste(non_numeric, collapse = ", "), ". ",
"Remove non-item columns (e.g., person IDs, grouping ",
"variables) before passing to plot_tile().",
call. = FALSE)
}
# Check values are integer-valued
all_vals <- unlist(data, use.names = FALSE)
all_vals <- all_vals[!is.na(all_vals)]
if (length(all_vals) == 0) {
stop("'data' contains only NA values.", call. = FALSE)
}
if (!all(all_vals == round(all_vals))) {
stop("All response values must be integers. ",
"Found non-integer values in the data.", call. = FALSE)
}
# Validate item_labels
if (!is.null(item_labels)) {
if (length(item_labels) != ncol(data)) {
stop("'item_labels' must have the same length as the number ",
"of columns in 'data'. Expected ", ncol(data),
", got ", length(item_labels), ".", call. = FALSE)
}
}
# Determine category range across all items
min_val <- min(all_vals)
max_val <- max(all_vals)
all_categories <- seq(min_val, max_val)
n_categories <- length(all_categories)
# Validate category_labels
if (!is.null(category_labels)) {
if (length(category_labels) != n_categories) {
stop("'category_labels' must have the same length as the ",
"number of response categories (", min_val, " to ",
max_val, " = ", n_categories, " categories). Got ",
length(category_labels), ".", call. = FALSE)
}
}
# ================================================================
# PREPARE DATA
# ================================================================
item_names <- names(data)
if (!is.null(item_labels)) {
label_map <- stats::setNames(item_labels, item_names)
} else {
label_map <- stats::setNames(item_names, item_names)
}
# Count responses per item x category, including zeros
count_list <- list()
for (item in item_names) {
item_vals <- data[[item]]
item_vals <- item_vals[!is.na(item_vals)]
tab <- table(factor(item_vals, levels = all_categories))
count_list[[item]] <- data.frame(
item = item,
category = as.integer(names(tab)),
n = as.integer(tab),
stringsAsFactors = FALSE
)
}
count_df <- do.call(rbind, count_list)
rownames(count_df) <- NULL
# Compute percentage within each item
item_totals <- stats::aggregate(n ~ item, data = count_df, FUN = sum)
colnames(item_totals)[2] <- "total"
count_df <- merge(count_df, item_totals, by = "item")
count_df$percentage <- round(count_df$n / count_df$total * 100, 1)
# Apply item labels
count_df$item_label <- label_map[count_df$item]
# Set factor levels: items in reverse column order (so first item
# appears at the top of the y-axis)
count_df$item_label <- factor(
count_df$item_label,
levels = rev(label_map[item_names])
)
# ================================================================
# BUILD PLOT
# ================================================================
p <- ggplot2::ggplot(
count_df,
ggplot2::aes(
x = .data$category,
y = .data$item_label,
fill = .data$n
)
) +
ggplot2::geom_tile() +
ggplot2::scale_fill_viridis_c(
expression(italic(n)),
limits = c(0, NA)
) +
ggplot2::labs(y = "Items") +
ggplot2::theme_minimal() +
ggplot2::theme(
axis.text.x = ggplot2::element_text(size = 8),
panel.grid = ggplot2::element_blank()
)
# X-axis: category labels or numeric breaks
if (!is.null(category_labels)) {
p <- p + ggplot2::scale_x_continuous(
"Response category",
expand = c(0, 0),
breaks = all_categories,
labels = category_labels
)
} else {
p <- p + ggplot2::scale_x_continuous(
"Response category",
expand = c(0, 0),
breaks = all_categories
)
}
# Cell labels: count or percentage, with optional highlighting
if (percent) {
if (highlight) {
p <- p +
ggplot2::geom_text(
ggplot2::aes(
label = paste0(.data$percentage, "%"),
color = ifelse(.data$n < cutoff, "red", text_color)
)
) +
ggplot2::guides(color = "none") +
ggplot2::scale_color_identity()
} else {
p <- p +
ggplot2::geom_text(
ggplot2::aes(label = paste0(.data$percentage, "%")),
color = text_color
)
}
} else {
if (highlight) {
p <- p +
ggplot2::geom_text(
ggplot2::aes(
label = .data$n,
color = ifelse(.data$n < cutoff, "red", text_color)
)
) +
ggplot2::guides(color = "none") +
ggplot2::scale_color_identity()
} else {
p <- p +
ggplot2::geom_text(
ggplot2::aes(label = .data$n),
color = text_color
)
}
}
p
}
#' Item Response Distribution Bar Chart
#'
#' Creates a faceted bar chart showing the response distribution for
#' each item, with counts and percentages displayed on each bar.
#' Each item gets its own panel, with response categories on the
#' x-axis and percentage of responses on the y-axis. This is a
#' descriptive data visualization tool intended for use before
#' model fitting.
#'
#' @param data A data frame in wide format containing only the item
#' response columns. Each column is one item, each row is one
#' person. All columns must be numeric (integer-valued). Response
#' categories may be coded starting from 0 or 1. Do not include
#' person IDs, grouping variables, or other non-item columns.
#' @param item_labels An optional character vector of descriptive
#' labels for the items (facet strips). Must be the same length
#' as \code{ncol(data)}. If \code{NULL} (the default), column
#' names are used. Labels are displayed as
#' \code{"column_name - label"}.
#' @param category_labels An optional character vector of labels
#' for the response categories (x-axis). Must be the same length
#' as the number of response categories spanning from the minimum
#' to the maximum observed value. If \code{NULL} (the default),
#' numeric category values are used.
#' @param ncol Integer. Number of columns in the faceted layout.
#' Default is 1.
#' @param label_wrap Integer. Number of characters per line in
#' facet strip labels before wrapping. Default is 25.
#' @param text_y Numeric. Vertical position (in percent units) for
#' the count labels on each bar. Adjust upward if bars are tall.
#' Default is 6.
#' @param viridis_option Character. Viridis palette option for the
#' count text color. One of \code{"A"} through \code{"H"}.
#' Default is \code{"A"}.
#' @param viridis_end Numeric in \eqn{[0, 1]}. End point of the
#' viridis color scale for count text. Adjust if text is hard to
#' read against the bar colors. Default is 0.9.
#' @param font Character. Font family for all text. Default is
#' \code{"sans"}.
#'
#' @return A \code{\link[ggplot2]{ggplot}} object.
#'
#' @details
#' Each item is displayed as a separate facet panel with the item
#' label in the strip on the left side. Bars are colored by response
#' category using the viridis palette. Each bar shows the count
#' (\code{n = X}) as text.
#'
#' \strong{Input requirements:}
#' \itemize{
#' \item All columns must be numeric (integer-valued).
#' \item The data frame must contain at least 2 columns (items)
#' and at least 1 row (person).
#' }
#'
#' @examples
#' library(ggplot2)
#' if (requireNamespace("eRm", quietly = TRUE))
#'
#' # Basic response distribution plot
#' plot_bars(eRm::pcmdat2)
#'
#' # With custom item labels
#' plot_bars(
#' eRm::pcmdat2,
#' item_labels = c("Mood", "Sleep", "Appetite", "Energy")
#' )
#'
#' # Two-column layout with wrapped labels
#' plot_bars(
#' eRm::pcmdat2,
#' item_labels = c(
#' "General mood and emotional wellbeing",
#' "Quality of sleep at night",
#' "Appetite and eating habits",
#' "Overall energy level during the day"
#' ),
#' ncol = 2, label_wrap = 20
#' )
#'
#' # With custom category labels
#' plot_bars(
#' eRm::pcmdat2,
#' category_labels = c("Never", "Sometimes", "Often")
#' )
#'
#' @importFrom rlang .data
#' @export
plot_bars <- function(
data,
item_labels = NULL,
category_labels = NULL,
ncol = 1,
label_wrap = 25,
text_y = 6,
viridis_option = "A",
viridis_end = 0.9,
font = "sans"
) {
if (!requireNamespace("ggplot2", quietly = TRUE)) {
stop("Package 'ggplot2' is required.", call. = FALSE)
}
# ================================================================
# INPUT VALIDATION
# ================================================================
if (!is.data.frame(data)) {
stop("'data' must be a data frame in wide format ",
"(one column per item, one row per person).",
call. = FALSE)
}
if (ncol(data) < 2) {
stop("'data' must contain at least 2 columns (items). ",
"Found ", ncol(data), " column(s).", call. = FALSE)
}
if (nrow(data) < 1) {
stop("'data' must contain at least 1 row (person). ",
"Found 0 rows.", call. = FALSE)
}
non_numeric <- names(data)[!vapply(data, is.numeric, logical(1))]
if (length(non_numeric) > 0) {
stop("All columns must be numeric. Non-numeric column(s): ",
paste(non_numeric, collapse = ", "), ". ",
"Remove non-item columns (e.g., person IDs, grouping ",
"variables) before passing to plot_bars().",
call. = FALSE)
}
all_vals <- unlist(data, use.names = FALSE)
all_vals <- all_vals[!is.na(all_vals)]
if (length(all_vals) == 0) {
stop("'data' contains only NA values.", call. = FALSE)
}
if (!all(all_vals == round(all_vals))) {
stop("All response values must be integers. ",
"Found non-integer values in the data.", call. = FALSE)
}
item_names <- names(data)
if (!is.null(item_labels)) {
if (length(item_labels) != length(item_names)) {
stop("'item_labels' must have the same length as the number ",
"of columns in 'data'. Expected ", length(item_names),
", got ", length(item_labels), ".", call. = FALSE)
}
}
min_val <- min(all_vals)
max_val <- max(all_vals)
all_categories <- seq(min_val, max_val)
n_categories <- length(all_categories)
if (!is.null(category_labels)) {
if (length(category_labels) != n_categories) {
stop("'category_labels' must have the same length as the ",
"number of response categories (", min_val, " to ",
max_val, " = ", n_categories, " categories). Got ",
length(category_labels), ".", call. = FALSE)
}
}
# ================================================================
# PREPARE DATA
# ================================================================
# Build facet labels: "itemnr - description" or just "itemnr"
if (!is.null(item_labels)) {
facet_labels <- paste0(item_names, " - ", item_labels)
} else {
facet_labels <- item_names
}
# Count responses per item x category, including zeros
count_list <- list()
for (i in seq_along(item_names)) {
item <- item_names[i]
item_vals <- data[[item]]
item_vals <- item_vals[!is.na(item_vals)]
tab <- table(factor(item_vals, levels = all_categories))
total <- sum(tab)
count_list[[item]] <- data.frame(
item_name = item,
facet_label = facet_labels[i],
category = as.integer(names(tab)),
n = as.integer(tab),
percent = if (total > 0) {
round(as.integer(tab) / total * 100, 1)
} else {
rep(0, length(tab))
},
stringsAsFactors = FALSE
)
}
plot_df <- do.call(rbind, count_list)
rownames(plot_df) <- NULL
# Factor levels preserve column order
plot_df$facet_label <- factor(
plot_df$facet_label,
levels = facet_labels
)
plot_df$category_fct <- factor(
plot_df$category,
levels = all_categories
)
# Apply category labels to the factor if provided
if (!is.null(category_labels)) {
levels(plot_df$category_fct) <- category_labels
}
# ================================================================
# BUILD PLOT
# ================================================================
p <- ggplot2::ggplot(
plot_df,
ggplot2::aes(x = .data$category_fct, y = .data$percent)
) +
ggplot2::geom_col(
ggplot2::aes(fill = .data$category_fct)
) +
ggplot2::geom_text(
ggplot2::aes(
label = paste0("n = ", .data$n),
color = .data$category_fct
),
y = text_y
) +
ggplot2::facet_wrap(
~ facet_label,
ncol = ncol,
strip.position = "left",
labeller = ggplot2::labeller(
facet_label = ggplot2::label_wrap_gen(label_wrap)
)
) +
ggplot2::scale_fill_viridis_d(guide = "none") +
ggplot2::scale_color_viridis_d(
guide = "none",
direction = -1,
option = viridis_option,
end = viridis_end
) +
ggplot2::scale_y_continuous(position = "right") +
ggplot2::labs(
x = "Response category",
y = "% of responses"
) +
ggplot2::theme_bw(base_family = font) +
ggplot2::theme(
legend.position = "none",
strip.text.y.left = ggplot2::element_text(angle = 0)
)
p
}
#' Stacked Bar Chart of Item Response Distributions
#'
#' Creates a horizontal stacked bar chart showing the response
#' distribution for all items. Each bar represents one item, with
#' segments colored by response category. Counts are displayed as
#' text labels within each segment. This is a descriptive data
#' visualization tool intended for use before model fitting.
#'
#' @param data A data frame in wide format containing only the item
#' response columns. Each column is one item, each row is one
#' person. All columns must be numeric (integer-valued). Response
#' categories may be coded starting from 0 or 1. Do not include
#' person IDs, grouping variables, or other non-item columns.
#' @param item_labels An optional character vector of descriptive
#' labels for the items (y-axis). Must be the same length as
#' \code{ncol(data)}. If \code{NULL} (the default), column names
#' are used.
#' @param category_labels An optional character vector of labels
#' for the response categories (legend). Must be the same length
#' as the number of response categories spanning from the minimum
#' to the maximum observed value, ordered from lowest to highest
#' category. If \code{NULL} (the default), numeric category
#' values are used.
#' @param show_n Logical. If \code{TRUE} (the default), the count
#' of responses is displayed as a text label inside each bar
#' segment.
#' @param show_percent Logical. If \code{TRUE}, the percentage of
#' responses is displayed instead of (or in addition to) counts.
#' Default is \code{FALSE}.
#' @param text_color Character. Color for the count/percentage
#' labels. Default is \code{"sienna1"}.
#' @param text_size Numeric. Size of the count/percentage labels.
#' Default is 3.
#' @param min_label_n Integer. Minimum count required for a label
#' to be displayed within a bar segment. Segments with fewer
#' responses are left unlabelled to avoid clutter. Default is 0
#' (all segments labelled).
#' @param viridis_option Character. Viridis palette option. One of
#' \code{"A"} through \code{"H"}. Default is \code{"D"}.
#' @param viridis_end Numeric in \eqn{[0, 1]}. End point of the
#' viridis color scale. Default is 0.99.
#' @param title Character. Plot title. Default is
#' \code{"Item responses"}.
#'
#' @return A \code{\link[ggplot2]{ggplot}} object.
#'
#' @details
#' Items are displayed on the y-axis in the same order as the
#' columns in \code{data} (first column at the top). Each bar is
#' divided into segments representing response categories, with
#' the lowest category on the left and the highest on the right.
#' The total bar length equals the number of non-missing responses
#' for that item.
#'
#' Categories with zero responses still appear in the legend but
#' produce no visible bar segment, which helps identify gaps in
#' the response distribution.
#'
#' \strong{Input requirements:}
#' \itemize{
#' \item All columns must be numeric (integer-valued).
#' \item The data frame must contain at least 2 columns (items)
#' and at least 1 row (person).
#' }
#'
#' @examples
#' library(ggplot2)
#' if (requireNamespace("eRm", quietly = TRUE))
#'
#' # Basic stacked bar chart
#' plot_stackedbars(eRm::pcmdat2)
#'
#' # With custom item and category labels
#' plot_stackedbars(
#' eRm::pcmdat2,
#' item_labels = c("Mood", "Sleep", "Appetite", "Energy"),
#' category_labels = c("Never", "Sometimes", "Often")
#' )
#'
#' # Show percentages, suppress small segments
#' plot_stackedbars(
#' eRm::pcmdat2,
#' show_percent = TRUE,
#' show_n = FALSE,
#' min_label_n = 5
#' )
#'
#' @importFrom rlang .data
#' @export
plot_stackedbars <- function(
data,
item_labels = NULL,
category_labels = NULL,
show_n = TRUE,
show_percent = FALSE,
text_color = "sienna1",
text_size = 3,
min_label_n = 0,
viridis_option = "D",
viridis_end = 0.99,
title = "Item responses"
) {
if (!requireNamespace("ggplot2", quietly = TRUE)) {
stop("Package 'ggplot2' is required.", call. = FALSE)
}
# ================================================================
# INPUT VALIDATION
# ================================================================
if (!is.data.frame(data)) {
stop("'data' must be a data frame in wide format ",
"(one column per item, one row per person).",
call. = FALSE)
}
if (ncol(data) < 2) {
stop("'data' must contain at least 2 columns (items). ",
"Found ", ncol(data), " column(s).", call. = FALSE)
}
if (nrow(data) < 1) {
stop("'data' must contain at least 1 row (person). ",
"Found 0 rows.", call. = FALSE)
}
non_numeric <- names(data)[!vapply(data, is.numeric, logical(1))]
if (length(non_numeric) > 0) {
stop("All columns must be numeric. Non-numeric column(s): ",
paste(non_numeric, collapse = ", "), ". ",
"Remove non-item columns (e.g., person IDs, grouping ",
"variables) before passing to plot_stackedbars().",
call. = FALSE)
}
all_vals <- unlist(data, use.names = FALSE)
all_vals <- all_vals[!is.na(all_vals)]
if (length(all_vals) == 0) {
stop("'data' contains only NA values.", call. = FALSE)
}
if (!all(all_vals == round(all_vals))) {
stop("All response values must be integers. ",
"Found non-integer values in the data.", call. = FALSE)
}
item_names <- names(data)
if (!is.null(item_labels)) {
if (length(item_labels) != length(item_names)) {
stop("'item_labels' must have the same length as the number ",
"of columns in 'data'. Expected ", length(item_names),
", got ", length(item_labels), ".", call. = FALSE)
}
}
min_val <- min(all_vals)
max_val <- max(all_vals)
all_categories <- seq(min_val, max_val)
n_categories <- length(all_categories)
if (!is.null(category_labels)) {
if (length(category_labels) != n_categories) {
stop("'category_labels' must have the same length as the ",
"number of response categories (", min_val, " to ",
max_val, " = ", n_categories, " categories). Got ",
length(category_labels), ".", call. = FALSE)
}
}
# ================================================================
# PREPARE DATA
# ================================================================
# Build y-axis labels
if (!is.null(item_labels)) {
y_labels <- stats::setNames(item_labels, item_names)
} else {
y_labels <- stats::setNames(item_names, item_names)
}
# Count responses per item x category, including zeros
count_list <- list()
for (item in item_names) {
item_vals <- data[[item]]
item_vals <- item_vals[!is.na(item_vals)]
tab <- table(factor(item_vals, levels = all_categories))
total <- sum(tab)
count_list[[item]] <- data.frame(
item_name = item,
category = as.integer(names(tab)),
n = as.integer(tab),
percent = if (total > 0) {
round(as.integer(tab) / total * 100, 1)
} else {
rep(0, length(tab))
},
stringsAsFactors = FALSE
)
}
plot_df <- do.call(rbind, count_list)
rownames(plot_df) <- NULL
# Item factor: reversed so first column appears at top of y-axis
plot_df$item_fct <- factor(
plot_df$item_name,
levels = rev(item_names),
labels = rev(y_labels[item_names])
)
# Category factor: reversed for stacking order (lowest category
# on the left = bottom of stack, which means it must be the
# last factor level in geom_col's default stacking)
plot_df$category_fct <- factor(
plot_df$category,
levels = rev(all_categories)
)
# Build text labels
plot_df$label <- ""
show_mask <- plot_df$n >= min_label_n
if (show_n && show_percent) {
plot_df$label[show_mask] <- paste0(
plot_df$n[show_mask], " (", plot_df$percent[show_mask], "%)"
)
} else if (show_n) {
plot_df$label[show_mask] <- as.character(plot_df$n[show_mask])
} else if (show_percent) {
plot_df$label[show_mask] <- paste0(
plot_df$percent[show_mask], "%"
)
}
# Suppress labels for zero-count segments (no visible bar)
plot_df$label[plot_df$n == 0] <- ""
# ================================================================
# BUILD LEGEND LABELS
# ================================================================
# Legend should show categories from lowest to highest (top to
# bottom), which is the reverse of the stacking factor
if (!is.null(category_labels)) {
legend_labels <- stats::setNames(
rev(category_labels),
rev(as.character(all_categories))
)
} else {
legend_labels <- stats::setNames(
rev(as.character(all_categories)),
rev(as.character(all_categories))
)
}
# ================================================================
# BUILD PLOT
# ================================================================
p <- ggplot2::ggplot(
plot_df,
ggplot2::aes(
x = .data$n,
y = .data$item_fct,
fill = .data$category_fct
)
) +
ggplot2::geom_col() +
ggplot2::scale_fill_viridis_d(
"Response\ncategory",
direction = -1,
option = viridis_option,
end = viridis_end,
labels = legend_labels
) +
ggplot2::labs(
title = title,
x = "Number of responses",
y = NULL
) +
ggplot2::theme_minimal() +
ggplot2::theme(
legend.position = "right"
)
# Add text labels if any content
if (show_n || show_percent) {
p <- p +
ggplot2::geom_text(
ggplot2::aes(label = .data$label),
color = text_color,
size = text_size,
position = ggplot2::position_stack(vjust = 0.5)
)
}
p
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.