# Building an Interactive Dashboard

This article shows how to build a self-contained HTML dashboard from Brightspace data using R Markdown and Chart.js. The result is a single HTML file you can open in any browser, share with colleagues, or host on a web server – no R or Shiny required to view it.

## Strategy

R Markdown is ideal for this: R chunks compute and aggregate data, then knitr’s inline R expressions inject the results directly into Chart.js JavaScript blocks. knitr knits the whole thing into a single self-contained HTML file.

## Step 1: Create the Rmd template

Create a file called `dashboard.Rmd` with this YAML front matter. The `params` block lets you render the same template for different date ranges.

    ---
    title: "Brightspace LMS Dashboard"
    output:
      html_document:
        self_contained: true
        theme: null
    params:
      from_date: !r Sys.Date() - 365
      to_date: !r Sys.Date()
    ---

## Step 2: Data preparation chunk

Add a hidden R chunk that loads and aggregates data. This chunk runs but produces no visible output (`include=FALSE`).

    # This chunk uses include=FALSE in the actual Rmd
    library(brightspaceR)
    library(dplyr)
    library(lubridate)
    library(jsonlite)

    # Helpers: convert R vectors to JS array literals
    js_labels <- function(x) toJSON(as.character(x), auto_unbox = FALSE)
    js_values <- function(x) toJSON(as.numeric(x), auto_unbox = FALSE)

    # Fetch datasets
    enrollments <- bs_get_dataset("User Enrollments")
    roles       <- bs_get_dataset("Role Details")
    grades      <- bs_get_dataset("Grade Results")
    org_units   <- bs_get_dataset("Org Units")
    users       <- bs_get_dataset("Users")

    # Apply date filter from params
    enrollments <- enrollments |>
      filter(
        as.Date(enrollment_date) >= params$from_date,
        as.Date(enrollment_date) <= params$to_date
      )

    # KPIs
    total_users <- format(nrow(users), big.mark = ",")
    total_enrol <- format(nrow(enrollments), big.mark = ",")
    n_courses   <- format(
      n_distinct(org_units$org_unit_id[org_units$type == "Course Offering"]),
      big.mark = ","
    )
    avg_grade <- grades |>
      filter(!is.na(points_numerator), points_numerator >= 0) |>
      summarise(m = round(mean(points_numerator, na.rm = TRUE), 1)) |>
      pull(m)

    # Chart data
    role_counts <- enrollments |>
      bs_join_enrollments_roles(roles) |>
      count(role_name, sort = TRUE) |>
      head(8)

    monthly_trend <- enrollments |>
      mutate(month = floor_date(as.Date(enrollment_date), "month")) |>
      count(month) |>
      arrange(month)

    grade_dist <- grades |>
      filter(!is.na(points_numerator), points_numerator >= 0) |>
      mutate(bracket = cut(points_numerator,
        breaks = seq(0, 100, 10), include.lowest = TRUE, right = FALSE
      )) |>
      count(bracket) |>
      filter(!is.na(bracket))

    top_courses <- enrollments |>
      bs_join_enrollments_orgunits(org_units) |>
      filter(type == "Course Offering") |>
      count(name, sort = TRUE) |>
      head(10)

## Step 3: HTML layout with inline R

Below the data chunk, add raw HTML for the dashboard layout. knitr evaluates inline R expressions everywhere in the document – including inside HTML tags and `<script>` blocks. The syntax is a backtick, the letter `r`, a space, an R expression, and a closing backtick.

The KPI cards use inline R to inject computed values. For example, writing the inline R syntax in the HTML outputs the formatted number:

    <div class="kpis">
      <div class="kpi">
        <div class="value" style="color:#818cf8">INLINE_R: total_users</div>
        <div class="label">Total Users</div>
      </div>
      <div class="kpi">
        <div class="value" style="color:#38bdf8">INLINE_R: total_enrol</div>
        <div class="label">Enrollments</div>
      </div>
      <!-- ... same pattern for n_courses, avg_grade -->
    </div>

Where `INLINE_R: total_users` represents knitr’s inline R syntax: a backtick, the letter r, a space, the expression, and a closing backtick. knitr replaces these with the evaluated value at render time.

The chart grid contains `<canvas>` elements for Chart.js:

    <div class="grid">
      <div class="card">
        <h2>Enrollments by Role</h2>
        <canvas id="roleChart"></canvas>
      </div>
      <div class="card">
        <h2>Monthly Enrollment Trend</h2>
        <canvas id="trendChart"></canvas>
      </div>
      <div class="card">
        <h2>Grade Distribution</h2>
        <canvas id="gradeChart"></canvas>
      </div>
      <div class="card">
        <h2>Top 10 Courses</h2>
        <canvas id="courseChart"></canvas>
      </div>
    </div>

## Step 4: Chart.js with inline R data

Load Chart.js from CDN, then initialise each chart. The key technique: inline R expressions inside `<script>` tags inject R data as JavaScript arrays.

    # In the Rmd, this is raw HTML (not an R chunk).
    # Inline R expressions like `r js_labels(...)` are replaced by knitr
    # with the evaluated JSON output before the HTML is finalised.
    #
    # Example Chart.js initialisation:
    #
    # <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    # <script>
    # new Chart("roleChart", {
    #   type: "doughnut",
    #   data: {
    #     labels: `r js_labels(role_counts$role_name)`,
    #     datasets: [{
    #       data: `r js_values(role_counts$n)`,
    #       backgroundColor: ["#38bdf8","#818cf8","#f59e0b","#34d399"]
    #     }]
    #   }
    # });
    # </script>

When knitr processes the Rmd, the inline R expressions are replaced with their evaluated results:

    Before knitr:
      labels: INLINE_R: js_labels(role_counts$role_name)

    After knitr:
      labels: ["Student","Instructor","TA","Observer"]

The `js_labels()` and `js_values()` helpers use `jsonlite::toJSON()` to produce valid JavaScript array literals. This is safer than manual `paste0()` because `toJSON()` handles quoting, escaping, and edge cases automatically.

The same pattern applies to each chart: line charts for trends, bar charts for distributions, horizontal bars for top courses. Each `new Chart()` call references a `<canvas>` id and uses inline R to inject labels and data arrays.

## Step 5: CSS styling

Add a `<style>` block at the top of the HTML section for the dashboard layout:

    <style>
      * { box-sizing: border-box; margin: 0; padding: 0; }
      body { font-family: system-ui, sans-serif; background: #f0f2f5; }
      .header { background: linear-gradient(135deg, #1a1a2e, #16213e);
                color: white; padding: 2rem; }
      .kpis { display: grid; grid-template-columns: repeat(4, 1fr);
              gap: 1rem; padding: 1.5rem; max-width: 1200px; margin: 0 auto; }
      .kpi { background: white; border-radius: 12px; padding: 1.2rem;
             text-align: center; }
      .kpi .value { font-size: 2rem; font-weight: 700; }
      .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr));
              gap: 1.5rem; padding: 0 1.5rem 1.5rem; max-width: 1200px; margin: 0 auto; }
      .card { background: white; border-radius: 12px; padding: 1.5rem; }
      .card h2 { font-size: 1rem; color: #475569; margin-bottom: 1rem; }
    </style>

## Rendering

    # Default: last 12 months
    rmarkdown::render("dashboard.Rmd",
      output_file = "brightspaceR_output/dashboard.html")
    browseURL("brightspaceR_output/dashboard.html")

## Parameterised reports

Render different versions from the same template without editing the Rmd:

    # This semester
    rmarkdown::render("dashboard.Rmd",
      params = list(from_date = as.Date("2026-01-01"), to_date = Sys.Date()),
      output_file = "brightspaceR_output/dashboard_s1_2026.html"
    )

    # Last year
    rmarkdown::render("dashboard.Rmd",
      params = list(from_date = as.Date("2025-01-01"), to_date = as.Date("2025-12-31")),
      output_file = "brightspaceR_output/dashboard_2025.html"
    )

This makes it straightforward to generate quarterly or per-semester reports from a single template.

## Using with the MCP server

The MCP server’s `execute_r` tool can use either approach:

1. Rmd rendering (recommended for complex dashboards): Write the Rmd file from `execute_r`, then call `rmarkdown::render()`. This produces the cleanest output and supports params.
2. HTML string (faster, simpler): Build HTML with `paste0()` and `writeLines()`. No Rmd dependency, but harder to maintain for complex layouts.

Both write to the output directory and can be opened with `browseURL()`.

## Why Chart.js instead of plotly?

| Chart.js | plotly
--- | --- | ---
Dependencies | None (CDN) | `plotly` R package + htmlwidgets
File size | ~80KB (CDN-loaded) | 3-5MB per file (self-contained)
Sharing | Single HTML, opens anywhere | Single HTML, but large
R Markdown | Works via inline R in `<script>` | Works via `plotly::ggplotly()`
MCP compatible | Yes (plain HTML string) | No (`htmlwidgets` needs R session)
Chart types | Bar, line, doughnut, scatter, radar | Everything, including 3D

For most LMS analytics – bar charts, line trends, doughnut breakdowns – Chart.js covers the use cases with zero R-side dependencies.