Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions rstudio/shiny-iris-dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Shiny Iris Dashboard

This example demonstrates how to build and run an interactive Shiny application from an RStudio environment. It uses the built-in Iris dataset, so no external downloads, API credentials, or object storage configuration are required.

The included `app.R` application demonstrates the following Shiny capabilities:

1. **Reactive Filtering:**
- Filter the Iris dataset by species, petal length, and sepal length.

2. **Interactive Visualization:**
- Choose the x/y variables for a scatter plot.
- Toggle an optional trend line.
- See plots update immediately when controls change.

3. **Reactive Summaries:**
- View grouped summary statistics for the currently filtered data.

4. **Interactive Data Table:**
- Browse, sort, and search the filtered records.

5. **Download Filtered Data:**
- Export the currently selected rows as a CSV file.

6. **Live Prediction Panel:**
- Enter flower measurements and classify them with a lightweight nearest-centroid classifier.
- Visualize the prediction against the original observations and species centroids.

## Running the App

Open `app.R` in RStudio and click **Run App**, or run:

```r
shiny::runApp("app.R", host = "0.0.0.0", port = 3838)
```

If you are already in the `shiny-iris-dashboard` directory, this shorter command is enough:

```r
shiny::runApp(host = "0.0.0.0", port = 3838)
```

## Notes

- The app is intentionally self-contained and uses only data bundled with R.
- This example is focused on running Shiny interactively from RStudio. A containerized Shiny deployment can be added separately if the app should be exposed as a standalone service.
225 changes: 225 additions & 0 deletions rstudio/shiny-iris-dashboard/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
library(shiny)
library(ggplot2)
library(dplyr)

# The Iris dataset ships with R, which keeps this example runnable without
# downloads, credentials, or access to external services.
iris_df <- as_tibble(iris)
numeric_columns <- c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")
species_levels <- levels(iris_df$Species)

# Simple in-memory classifier used to keep the app self-contained.
# Each species is represented by the average of its numeric measurements.
species_centroids <- iris_df %>%
group_by(Species) %>%
summarise(across(all_of(numeric_columns), mean), .groups = "drop")

# The UI defines the controls and output placeholders. Shiny automatically
# connects these input and output IDs to the server logic below.
ui <- fluidPage(
titlePanel("Shiny Iris Explorer"),
p(
"An interactive RStudio/Shiny dashboard for exploring the built-in Iris dataset, ",
"filtering records, inspecting plots, and trying a lightweight species prediction."
),
sidebarLayout(
sidebarPanel(
# These controls demonstrate reactive inputs: changing any of them
# invalidates dependent calculations and refreshes the affected outputs.
checkboxGroupInput(
inputId = "species",
label = "Species",
choices = species_levels,
selected = species_levels
),
sliderInput(
inputId = "petal_length",
label = "Petal length range",
min = min(iris_df$Petal.Length),
max = max(iris_df$Petal.Length),
value = range(iris_df$Petal.Length),
step = 0.1
),
sliderInput(
inputId = "sepal_length",
label = "Sepal length range",
min = min(iris_df$Sepal.Length),
max = max(iris_df$Sepal.Length),
value = range(iris_df$Sepal.Length),
step = 0.1
),
selectInput(
inputId = "x_var",
label = "X axis",
choices = numeric_columns,
selected = "Petal.Length"
),
selectInput(
inputId = "y_var",
label = "Y axis",
choices = numeric_columns,
selected = "Petal.Width"
),
checkboxInput(
inputId = "show_trend",
label = "Show trend line",
value = TRUE
)
),
mainPanel(
tabsetPanel(
tabPanel(
"Explore",
br(),
# Outputs are placeholders. Their contents are produced in the
# matching render* calls in the server function.
plotOutput("scatter_plot", height = "460px"),
h4("Filtered summary"),
verbatimTextOutput("summary_text")
),
tabPanel(
"Data",
br(),
downloadButton("download_filtered", "Download filtered CSV"),
br(),
br(),
dataTableOutput("iris_table")
),
tabPanel(
"Predict",
br(),
p("Adjust the flower measurements to classify the observation using nearest species centroids."),
fluidRow(
column(3, numericInput("predict_sepal_length", "Sepal length", value = 5.1, min = 0, step = 0.1)),
column(3, numericInput("predict_sepal_width", "Sepal width", value = 3.5, min = 0, step = 0.1)),
column(3, numericInput("predict_petal_length", "Petal length", value = 1.4, min = 0, step = 0.1)),
column(3, numericInput("predict_petal_width", "Petal width", value = 0.2, min = 0, step = 0.1))
),
h4(textOutput("prediction_text")),
plotOutput("prediction_plot", height = "380px")
)
)
)
)
)

server <- function(input, output, session) {
# A reactive expression stores shared filtering logic. Any output that calls
# filtered_iris() will update automatically when the selected inputs change.
filtered_iris <- reactive({
validate(need(length(input$species) > 0, "Select at least one species."))

iris_df %>%
filter(
Species %in% input$species,
between(Petal.Length, input$petal_length[1], input$petal_length[2]),
between(Sepal.Length, input$sepal_length[1], input$sepal_length[2])
)
})

output$scatter_plot <- renderPlot({
plot_data <- filtered_iris()
validate(need(nrow(plot_data) > 0, "No rows match the selected filters."))

# The selected column names are read from input$x_var and input$y_var.
# .data[[...]] lets ggplot use those dynamic column names safely.
plot <- ggplot(plot_data, aes(x = .data[[input$x_var]], y = .data[[input$y_var]], color = Species)) +
geom_point(size = 3, alpha = 0.75) +
labs(
title = "Interactive Iris feature comparison",
subtitle = paste(nrow(plot_data), "rows currently selected"),
x = input$x_var,
y = input$y_var
) +
theme_minimal(base_size = 13)

# The trend line is optional so users can see how a checkbox changes the
# rendered chart without changing the underlying data.
if (isTRUE(input$show_trend) && nrow(plot_data) > 2) {
plot <- plot + geom_smooth(method = "lm", se = FALSE, linewidth = 0.8)
}

plot
})

output$summary_text <- renderPrint({
plot_data <- filtered_iris()
validate(need(nrow(plot_data) > 0, "No rows match the selected filters."))

# This summary is recalculated for the currently filtered rows only.
plot_data %>%
group_by(Species) %>%
summarise(
rows = n(),
across(all_of(numeric_columns), ~ round(mean(.x), 2), .names = "mean_{.col}"),
.groups = "drop"
)
})

output$iris_table <- renderDataTable({
filtered_iris()
}, options = list(pageLength = 10, scrollX = TRUE))

# downloadHandler receives the same reactive data as the plot and table, so
# the downloaded file always matches the user's current filter state.
output$download_filtered <- downloadHandler(
filename = function() {
paste0("filtered-iris-", Sys.Date(), ".csv")
},
content = function(file) {
write.csv(filtered_iris(), file, row.names = FALSE)
}
)

# Collect the four numeric prediction inputs into one named vector. The names
# match numeric_columns, which keeps the distance calculation straightforward.
prediction_values <- reactive({
c(
Sepal.Length = input$predict_sepal_length,
Sepal.Width = input$predict_sepal_width,
Petal.Length = input$predict_petal_length,
Petal.Width = input$predict_petal_width
)
})

predicted_species <- reactive({
values <- prediction_values()
centroid_matrix <- as.matrix(species_centroids[, numeric_columns])

# Classify the input by finding the closest species centroid in feature
# space. This is intentionally simple and transparent for demonstration.
distances <- apply(centroid_matrix, 1, function(row) sqrt(sum((row - values) ^ 2)))

species_centroids$Species[which.min(distances)]
})

output$prediction_text <- renderText({
paste("Predicted species:", predicted_species())
})

output$prediction_plot <- renderPlot({
# The input point is plotted alongside the original data and species
# centroids so users can see why the nearest-centroid prediction changed.
input_point <- tibble(
Sepal.Length = input$predict_sepal_length,
Sepal.Width = input$predict_sepal_width,
Petal.Length = input$predict_petal_length,
Petal.Width = input$predict_petal_width,
Species = predicted_species()
)

ggplot(iris_df, aes(x = Petal.Length, y = Petal.Width, color = Species)) +
geom_point(alpha = 0.35) +
geom_point(data = species_centroids, shape = 4, size = 5, stroke = 1.5) +
geom_point(data = input_point, shape = 8, size = 6, stroke = 1.5) +
labs(
title = "Prediction compared with Iris observations",
subtitle = "Crosses are species centroids; the star is the measurement being classified.",
x = "Petal length",
y = "Petal width"
) +
theme_minimal(base_size = 13)
})
}

shinyApp(ui = ui, server = server)
88 changes: 88 additions & 0 deletions rstudio/storage_examples/connect_freetds_named_instance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Connecting to a SQL Server Named Instance with FreeTDS

## What is a Named Instance?

A single machine can run **multiple SQL Server installations** side by side.
Each installation is called an **instance**:

| Type | Example | Default Port |
|---|---|---|
| **Default instance** | `myserver.example.com` | **1433** (always) |
| **Named instance** | `myserver.example.com\PROD` | **Dynamic** (assigned at startup) |

The `\PROD` part after the hostname is the **instance name**. It is **not** part of
the hostname or domain — it tells the SQL Server Browser service which installation
on that machine you want to reach.

> **Key point:** A named instance does not listen on port 1433. It gets a
> dynamically assigned port each time it starts, unless a DBA has pinned it to a
> fixed port.

---

## The Problem with FreeTDS

**FreeTDS does not support the `hostname\INSTANCE` syntax.** Unlike the Microsoft
ODBC Driver, FreeTDS cannot resolve a named instance to its TCP port automatically.

This means you **must first discover the correct TCP port** of the named instance
yourself, and then use that port directly in your driver configuration.

---

## Step 1 — Find the TCP Port of the Named Instance

Open a **Terminal** in RStudio (*Tools > Terminal > New Terminal*) and run:

```bash
tsql -L mysql.prod.customer.tld
```

This queries the SQL Server Browser service and lists all instances on that host.
Example output:

```
ServerName MySQL
InstanceName PROD
IsClustered No
Version 14.0.3456.2
tcp 51433
```

Find the entry for your instance (`PROD`) and note the value of the **`tcp`
attribute** — this is the TCP port the instance is listening on. In this example,
the port is **51433**.

> If the Browser service (UDP 1434) is blocked by a firewall, `tsql -L` will time
> out. In that case, contact your DBA to provide the port.

---

## Step 2 — Use the Port in Your Connection

Take the `tcp` value from Step 1 and use it as the `Port` in `dbConnect()`:

```r
library(DBI)
library(odbc)
library(rstudioapi)

# --- Connection to named instance PROD (tcp port from Step 1) ---
con <- dbConnect(
odbc::odbc(),
Driver = "FreeTDS",
Server = "mysql.prod.customer.tld", # hostname only — do NOT add \PROD
Port = 51433, # tcp value from tsql -L output
Database = "your_database",
UID = "your_user",
PWD = askForPassword(prompt = "Please enter your password:"),
TDS_Version = "7.4" # use 7.4 for SQL Server 2017
)

# --- Query ---
result <- dbGetQuery(con, "SELECT TOP 10 * FROM your_table")
print(result)

# --- Disconnect ---
dbDisconnect(con)
```