Introduction

First and foremost, this report is intended to be a comprehensive record and guide for the development of the What-If Analysis Dashboard project. It is structured to guide readers through each stage of the project, including data preprocessing, model development, User Interface (UI) design, and the implementation of interactive features using R and the Shiny framework.

In general, What-If Analysis is a simulation method used in economics that allows users to explore potential outcomes of various scenarios by adjusting key indicators. This approach helps users understand how changes in one variable may influence others, predict possible outcomes under different assumptions, and support evidence-based decision-making in uncertain or dynamic environments.

As a practical application, the What-If Analysis Dashboard is an interactive web-based tool developed to simulate, compare, and analyze key economic indicators across Malaysian states. It leverages predictive modeling to generate real-time forecasts and scenario-based insights, allowing users to explore how changes in variables such as CPI, inflation, and GDP may affect broader economic outcomes. The dashboard may serve as a practical and dependable tool for researchers, policymakers, and analysts alike.

The dashboard is structured into three main tabs:

  • Scenario Simulation: Allows users to run simulations by adjusting key input values and observing predicted outputs instantly.

  • Scenario Comparison: Enables side-by-side comparisons of multiple user-defined scenarios across different states and economic indicators.

  • Analysis Results: Presents full data set, descriptive trends, correlation insights, predictive visuals and time series forecasts for more in-depth analysis.

Data Preprocessing

This section outlines the data preparation process conducted prior to the development of the What-If Analysis Dashboard. It includes loading required libraries, importing and cleaning raw data sets, merging relevant data, constructing reference tables, fitting predictive models, and labeling variables for later use in the user interface and visualizations.

Each preprocessing step is critical to ensure data integrity, modeling accuracy, and seamless integration with the Shiny dashboard’s components.

1. Load Required Libraries

We begin by loading all the necessary R packages required for the application. These include packages for Shiny UI, data wrangling, visualization, spatial analysis, forecasting, and interactivity.

# Core Shiny & UI components
library(shiny)
library(shinyWidgets)

# Visualization & Plotting
library(ggplot2)
library(ggthemes)
library(plotly)

# Data manipulation
library(dplyr)
library(tidyr)
library(purrr)
library(scales)
library(tibble)

# Time series handling
library(tsibble)
library(lubridate)
library(forecast)

# Interactive tables
library(DT)
library(sf)

# Mapping & Geospatial data sources
library(leaflet)
library(rnaturalearth)
library(rnaturalearthdata)
library(rnaturalearthhires)

# Color palettes
library(RColorBrewer)

2. Load Data Files

All input CSV files are located in the data directory. The code below loads each file into a list for batch processing and inspection.

# Define folder path and list all CSV files in data frames
folder_path = "data"
file_list = list.files(path = folder_path, pattern = "*.csv", full.names = TRUE)
data_list = lapply(file_list, read.csv)

# List of data sets
file_list
## [1] "data/cpi_yearly_state_division.csv"      
## [2] "data/gdp_yearly_state_sector.csv"        
## [3] "data/inflation_yearly_state_division.csv"
## [4] "data/wages_yearly_state.csv"             
## [5] "data/zReference_1.csv"                   
## [6] "data/zReference_2.csv"                   
## [7] "data/zReference_3.csv"
# The structure of each data sets
str(data_list)
## List of 7
##  $ :'data.frame':    2142 obs. of  4 variables:
##   ..$ year    : int [1:2142] 2015 2016 2017 2018 2019 2020 2021 2022 2023 2015 ...
##   ..$ state   : chr [1:2142] "Malaysia" "Malaysia" "Malaysia" "Malaysia" ...
##   ..$ division: chr [1:2142] "D00" "D00" "D00" "D00" ...
##   ..$ index   : num [1:2142] 113 115 120 121 121 ...
##  $ :'data.frame':    1071 obs. of  5 variables:
##   ..$ year      : int [1:1071] 2015 2016 2017 2018 2019 2020 2021 2022 2023 2015 ...
##   ..$ state     : chr [1:1071] "Malaysia" "Malaysia" "Malaysia" "Malaysia" ...
##   ..$ sector    : chr [1:1071] "S0" "S0" "S0" "S0" ...
##   ..$ gdp       : num [1:1071] 1176941 1229312 1300769 1363766 1423952 ...
##   ..$ gdp_capita: num [1:1071] 37739 38861 40620 42115 43783 ...
##  $ :'data.frame':    2142 obs. of  4 variables:
##   ..$ year     : int [1:2142] 2015 2016 2017 2018 2019 2020 2021 2022 2023 2015 ...
##   ..$ state    : chr [1:2142] "Malaysia" "Malaysia" "Malaysia" "Malaysia" ...
##   ..$ division : chr [1:2142] "D00" "D00" "D00" "D00" ...
##   ..$ inflation: num [1:2142] 2.104 2.076 3.799 0.969 0.663 ...
##  $ :'data.frame':    153 obs. of  4 variables:
##   ..$ year        : int [1:153] 2015 2016 2017 2018 2019 2020 2021 2022 2023 2015 ...
##   ..$ state       : chr [1:153] "Malaysia" "Malaysia" "Malaysia" "Malaysia" ...
##   ..$ mean_wages  : int [1:153] 2487 2657 2879 3087 3224 2954 3049 3219 3441 2549 ...
##   ..$ median_wages: int [1:153] 1942 2000 2160 2308 2442 2076 2256 2429 2602 2200 ...
##  $ :'data.frame':    153 obs. of  6 variables:
##   ..$ year      : int [1:153] 2015 2016 2017 2018 2019 2020 2021 2022 2023 2015 ...
##   ..$ state     : chr [1:153] "Malaysia" "Malaysia" "Malaysia" "Malaysia" ...
##   ..$ index     : num [1:153] 113 115 120 121 122 ...
##   ..$ inflation : num [1:153] 2.1 2.1 3.8 1 0.7 -1.1 2.5 3.4 2.5 2.8 ...
##   ..$ gdp       : int [1:153] 1176900 1229300 1300800 1363800 1424000 1346200 1390900 1514100 1568000 110000 ...
##   ..$ gdp_capita: int [1:153] 37700 38900 40600 42100 43800 41500 42700 46300 46900 29500 ...
##  $ :'data.frame':    153 obs. of  6 variables:
##   ..$ year        : int [1:153] 2015 2016 2017 2018 2019 2020 2021 2022 2023 2015 ...
##   ..$ state       : chr [1:153] "Malaysia" "Malaysia" "Malaysia" "Malaysia" ...
##   ..$ gdp         : int [1:153] 1176900 1229300 1300800 1363800 1424000 1346200 1390900 1514100 1568000 110000 ...
##   ..$ gdp_capita  : int [1:153] 37700 38900 40600 42100 43800 41500 42700 46300 46900 29500 ...
##   ..$ mean_wages  : int [1:153] 2490 2660 2880 3090 3220 2950 3050 3220 3440 2550 ...
##   ..$ median_wages: int [1:153] 1940 2000 2160 2310 2440 2080 2260 2430 2600 2200 ...
##  $ :'data.frame':    153 obs. of  6 variables:
##   ..$ year        : int [1:153] 2015 2016 2017 2018 2019 2020 2021 2022 2023 2015 ...
##   ..$ state       : chr [1:153] "Malaysia" "Malaysia" "Malaysia" "Malaysia" ...
##   ..$ index       : num [1:153] 113 115 120 121 122 ...
##   ..$ inflation   : num [1:153] 2.1 2.1 3.8 1 0.7 -1.1 2.5 3.4 2.5 2.8 ...
##   ..$ mean_wages  : int [1:153] 2490 2660 2880 3090 3220 2950 3050 3220 3440 2550 ...
##   ..$ median_wages: int [1:153] 1940 2000 2160 2310 2440 2080 2260 2430 2600 2200 ...

3. Clean & Standardize Data

Each data set is cleaned by removing rows with missing values, converting character columns to factors, and standardizing state levels to ensure “Malaysia” always appears first.

# Remove missing values from each data set
data_list = lapply(data_list, function(df) {
  df = na.omit(df)
  df
})

# Convert character columns to factors
data_list = lapply(data_list, function(df) {
  df[] = lapply(df, function(col) {
    if (is.character(col)) factor(col) 
    else col})
  df
})

# Reorder states: Malaysia appears first, followed by other states alphabetically
data_list = lapply(data_list, function(df) {
  states = sort(setdiff(unique(as.character(df$state)), "Malaysia"))
  df$state = factor(df$state, levels = c("Malaysia", states))
  df
})

4. Extract & Merge Data

Relevant subsets are extracted from individual data sets (CPI, Inflation, Wages, GDP) and merged into a single merged_df data set for modeling and visualization.

# Extract CPI data
cpi_df = data_list[[1]] %>% 
  filter(division == "D00") %>% 
  select(year, state, cpi = index)

# Extract Inflation data
inflation_df = data_list[[3]] %>% 
  filter(division == "D00") %>% 
  select(year, state, inflation)

# Extract GDP and GDP per capita data
gdp_df = data_list[[2]] %>% 
  filter(sector == "S0") %>% 
  select(year, state, gdp, gdp_capita)

# Extract wages data
wages_df = data_list[[4]] %>% 
  select(year, state, mean_wages, median_wages)

# Merge all data by year and state
merged_df = reduce(list(wages_df, cpi_df, inflation_df, gdp_df), 
                   left_join, by = c("year", "state"))

# The preview of the merged data set
head(merged_df)
##   year    state mean_wages median_wages      cpi  inflation     gdp gdp_capita
## 1 2015 Malaysia       2487         1942 112.8083  2.1043898 1176941      37739
## 2 2016 Malaysia       2657         2000 115.1500  2.0757923 1229312      38861
## 3 2017 Malaysia       2879         2160 119.5250  3.7993921 1300769      40620
## 4 2018 Malaysia       3087         2308 120.6833  0.9691139 1363766      42115
## 5 2019 Malaysia       3224         2442 121.4833  0.6628919 1423952      43783
## 6 2020 Malaysia       2954         2076 120.1000 -1.1387022 1346249      41490
# The summary of the merged data set
summary(merged_df)
##       year                  state      mean_wages    median_wages 
##  Min.   :2015   Malaysia       : 9   Min.   :2024   Min.   :1200  
##  1st Qu.:2017   Johor          : 9   1st Qu.:2507   1st Qu.:1757  
##  Median :2019   Kedah          : 9   Median :2840   Median :2065  
##  Mean   :2019   Kelantan       : 9   Mean   :2943   Mean   :2179  
##  3rd Qu.:2021   Melaka         : 9   3rd Qu.:3218   3rd Qu.:2429  
##  Max.   :2023   Negeri Sembilan: 9   Max.   :4858   Max.   :4443  
##                 (Other)        :99                                
##       cpi          inflation            gdp            gdp_capita    
##  Min.   :109.3   Min.   :-2.1167   Min.   :   5353   Min.   : 12075  
##  1st Qu.:115.5   1st Qu.: 0.6629   1st Qu.:  39550   1st Qu.: 27268  
##  Median :119.4   Median : 1.9750   Median :  73776   Median : 41254  
##  Mean   :119.8   Mean   : 1.7023   Mean   : 171096   Mean   : 47597  
##  3rd Qu.:123.2   3rd Qu.: 2.7833   3rd Qu.: 142352   3rd Qu.: 54725  
##  Max.   :138.3   Max.   : 7.2833   Max.   :1567974   Max.   :129472  
## 

5. Create Reference Tables

Separate reference tables are created for use in the dashboard’s data tables.

# Reference table 1: CPI, inflation, and GDP
ref1_df = data_list[[5]] %>% select(year, state, cpi = index, inflation, gdp, gdp_capita)

# Reference table 2: GDP and wages
ref2_df = data_list[[6]] %>% select(year, state, gdp, gdp_capita, mean_wages, median_wages)

# Reference table 3: CPI, inflation and wages
ref3_df = data_list[[7]] %>% select(year, state, cpi = index, inflation, mean_wages, median_wages)

# The preview for each table
head(ref1_df)
##   year    state   cpi inflation     gdp gdp_capita
## 1 2015 Malaysia 112.8       2.1 1176900      37700
## 2 2016 Malaysia 115.2       2.1 1229300      38900
## 3 2017 Malaysia 119.5       3.8 1300800      40600
## 4 2018 Malaysia 120.7       1.0 1363800      42100
## 5 2019 Malaysia 121.5       0.7 1424000      43800
## 6 2020 Malaysia 120.1      -1.1 1346200      41500
head(ref2_df)
##   year    state     gdp gdp_capita mean_wages median_wages
## 1 2015 Malaysia 1176900      37700       2490         1940
## 2 2016 Malaysia 1229300      38900       2660         2000
## 3 2017 Malaysia 1300800      40600       2880         2160
## 4 2018 Malaysia 1363800      42100       3090         2310
## 5 2019 Malaysia 1424000      43800       3220         2440
## 6 2020 Malaysia 1346200      41500       2950         2080
head(ref3_df)
##   year    state   cpi inflation mean_wages median_wages
## 1 2015 Malaysia 112.8       2.1       2490         1940
## 2 2016 Malaysia 115.2       2.1       2660         2000
## 3 2017 Malaysia 119.5       3.8       2880         2160
## 4 2018 Malaysia 120.7       1.0       3090         2310
## 5 2019 Malaysia 121.5       0.7       3220         2440
## 6 2020 Malaysia 120.1      -1.1       2950         2080

6. Build Predictive Models

Multiple Linear Regression models are trained using the merged data set to predict various economic indicators. These models are later used in the What-If Simulation feature of the dashboard.

# Wages prediction models
mean_model   = lm(mean_wages ~ cpi + inflation + gdp + gdp_capita + state, data = merged_df)
median_model = lm(median_wages ~ cpi + inflation + gdp + gdp_capita + state, data = merged_df)

# CPI and Inflation prediction model
cpi_model    = lm(cpi ~ gdp + gdp_capita + state, data = merged_df)
inf_model    = lm(inflation ~ gdp + gdp_capita + state, data = merged_df)

# GDP and GDP per capita prediction model
gdp_model    = lm(gdp ~ cpi + inflation + state, data = merged_df)
capita_model = lm(gdp_capita ~ cpi + inflation + state, data = merged_df)

7. Variable Labelling

A named list is created to store readable variable titles and y-axis labels. These labels are dynamically used in charts and visual outputs in the dashboard.

# Easy-to-read labels for charts and outputs
variable_labels = list(
  mean_wages   = list(title = "Mean Wages", y_title = "Wages (MYR)"),
  median_wages = list(title = "Median Wages", y_title = "Wages (MYR)"),
  gdp          = list(title = "GDP", y_title = "GDP (MYR Million)"),
  gdp_capita   = list(title = "GDP per capita", y_title = "GDP per capita (MYR Million)"),
  cpi          = list(title = "CPI", y_title = "Consumer Price Index"),
  inflation    = list(title = "Inflation Rate", y_title = "Inflation Rate (%)")
)

User Interface (UI) Design

The UI for this dashboard is constructed using the dashboardPage() function from the shinydashboard package, which combines the header, sidebar, and body into a complete dashboard layout. This section outlines the design and functionality of each component.

First and foremost, the overall dashboard page can be titled using the title parameter,

# UI components structure
ui = dashboardPage(
  title = "Economic Indicators What-If Analysis Dashboard",
  dashboardHeader(...),
  dashboardSidebar(...),
  dashboardBody(...)
)

1. Dashboard Header

The dashboard header is defined using dashboardHeader() function. It includes a logo linked to Media Smart Resources’ homepage and a prominent dashboard title, styled with CSS for alignment and spacing.

# Header with Media Smart Resources logo and custom title
dashboardHeader(
    title = tags$div(
      style = "display: flex; align-items: center; width: 100%;",
      tags$a(href = "https://mediasmart.my/v2/main-page/", target = "_blank",
             tags$img(src = "msrsb_logo.png", height = "50px", 
                      style = "margin-left: -15px; margin-right: 15px;")),
      tags$span("What-If Analysis Dashboard",
                style = "font-size: 21px; font-weight: bold; flex-grow: 1;")),
    titleWidth = 500
  )

2. Dashboard Sidebar

The sidebar is implemented using the dashboardSidebar() function and includes a sidebarMenu() to enable user navigation across the three main features:

  • What-If Simulation

  • Scenario Comparison

  • Analysis Results

# Sidebar navigation menu with three main sections
dashboardSidebar(
    sidebarMenu(
      menuItem("What-If Simulation", tabName = "inputs", icon = icon("sliders")),
      menuItem("Scenario Comparison", tabName = "scenario", icon = icon("table")),
      menuItem("Analysis Results", tabName = "results", icon = icon("chart-line"))
    )
  )

3. Dashboard Body

The main content is defined using the dashboardBody() function. Within it, the tabItems() function manages three individual tabItem() sections, each corresponding to a sidebar menu item.

A. Tab 1 Layout

This tab allows users to choose a prediction mode, input parameters dynamically, and simulate results.

tabItem(tabName = "inputs",
              fluidRow(
                # Side panel: Prediction mode selection and inputs
                column(width = 4,
                       box(title = "What-If Settings", width = 12,
                           status = "primary", solidHeader = TRUE,
                           radioButtons("predict_mode", "Prediction Mode",
                                        choices = c("Salaries & Wages" = "p_wage", 
                                                    "Price Indexes & Inflation" = "p_price",
                                                    "Gross Domestic Product" = "p_gdp"), 
                                        selected = "p_wage"),
                           uiOutput("dynamic_inputs"),
                           actionButton("run_sim", "Run Simulation", icon = icon("play")))
                ),
                # Main panel: Prediction results and historical reference data
                column(width = 8,
                       box(title = "What-If Predictions", width = 12, 
                           status = "success", solidHeader = TRUE,
                           valueBoxOutput("predictionBox1", width = 12),
                           valueBoxOutput("predictionBox2", width = 12)),
                       box(title = "Reference Table", width = 12,
                           status = "info", solidHeader = TRUE,
                           DTOutput("ref_table1"))
                )
              )
      )

B. Tab 2 Layout

This section enables the user to save and compare multiple scenarios under different economic hypotheticals.

tabItem(tabName = "scenario",
              fluidRow(
                # Side panel: Scenario input and saving
                column(width = 4,
                       box(title = "Scenario Settings", width = 12,
                           status = "primary", solidHeader = TRUE,
                           radioButtons("scenario_mode", "Scenario Mode",
                                        choices = c("Salaries & Wages" = "p_wage", 
                                                    "Price Indexes & Inflation" = "p_price",
                                                    "Gross Domestic Product" = "p_gdp"), 
                                        selected = "p_wage"),
                           textInput("scenario_name", "Scenario Name"),
                           uiOutput("scenario_inputs"),
                           actionButton("save_scenario", "Save Scenario", icon = icon("save")),
                           br(), br(),
                           uiOutput("scenario_selector_ui"))
                ),
                # Main panel: Comparison table and historical reference data
                column(width = 8,
                       box(title = "Scenario Comparison Table", width = 12,
                           status = "success", solidHeader = TRUE,
                           DTOutput("comp_table")),
                       box(title = "Reference Table", width = 12,
                           status = "info", solidHeader = TRUE,
                           DTOutput("ref_table2"))
                )
              )
      )

C. Tab 3 Layout

This comprehensive analysis results tab includes various subsections:

  • Full data table
  • Descriptive analysis charts
  • What-If Analysis visuals
  • Time series forecasting
tabItem(tabName = "results",
              fluidRow(
                # Settings for variable, state, and year range
                box(title = "Result Settings", width = 12,
                    status = "primary", solidHeader = TRUE,
                    fluidRow(
                      column(width = 6,
                             selectInput("selected_variable", "Select Variable",
                                         choices = c("Mean Wages" = "mean_wages", 
                                                     "Median Wages" = "median_wages",
                                                     "CPI" = "cpi", 
                                                     "Inflation Rate" = "inflation",
                                                     "GDP" = "gdp", 
                                                     "GDP per capita" = "gdp_capita"))),
                      column(width = 6,
                             selectInput("selected_state3", "Select State",
                                         choices = unique(merged_df$state), 
                                         selected = "Malaysia"))),
                    fluidRow(
                      column(width = 12,
                             sliderInput("year_range", "Select Year Range", 
                                         min = 2015, max = 2023,
                                         value = c(2015, 2023), step = 1, sep = "",
                                         dragRange = TRUE))))
              ),
              fluidRow(
                # Full data table with data sources
                box(title = "Full Data Table", width = 12,
                    status = "warning", solidHeader = TRUE,
                    DTOutput("table"),
                    tags$div(
                      style = "font-size: 12px; color: gray;",
                      HTML(
                        "Data Sources:<br>
                        <a href='https://open.dosm.gov.my/data-catalogue' target='_blank' 
                           style='color: gray; text-decoration: underline;'>
                           Department of Statistics Malaysia (DOSM)</a>, 2015–2023<br>
                        <a href='https://www.ceicdata.com/en/malaysia/salaries--wages-survey-by-states'
                           target='_blank' 
                           style='color: gray; text-decoration: underline;'>
                           Bank Negara Malaysia (BNM)</a>, 2015–2023")))
              ),
              fluidRow(
                # Trend analysis and correlation visualization
                box(title = "Descriptive Analysis", width = 12,
                    status = "success", solidHeader = TRUE,
                    fluidRow(
                      column(width = 6, plotlyOutput("trend_plot")),
                      column(width = 6, plotlyOutput("correlation_heatmap"))))
              ),
              fluidRow(
                # Dynamic What-If analysis via bar and choropleth map
                box(title = "What-If Analysis", width = 12,
                    status = "success", solidHeader = TRUE,
                    plotlyOutput("bar_plot", height = "510px"),
                    br(),
                    leafletOutput("map_plot"))
              ),
              fluidRow(
                # Forecast settings and time series plot
                box(title = "Forecast Settings", width = 4, 
                    status = "primary", solidHeader = TRUE,
                    selectInput("forecast_variable", "Select Variable", 
                                choices = c("Mean Wages" = "mean_wages", 
                                            "Median Wages" = "median_wages",
                                            "CPI" = "cpi", 
                                            "Inflation Rate" = "inflation",
                                            "GDP" = "gdp", 
                                            "GDP per capita" = "gdp_capita")),
                    selectInput("forecast_state", "Select State", 
                                choices = unique(merged_df$state), selected = "Malaysia"),
                    numericInput("forecast_horizon", "Forecast Horizon (Years)", 
                                 value = 5, min = 1, max = 30),
                    actionButton("run_forecast", "Run Forecast", 
                                 icon = icon("chart-line"))),
                box(title = "Time Series Analysis", width = 8,
                    status = "success", solidHeader = TRUE,
                    plotlyOutput("forecast_plot", height = "470px"))
              )
      )

D. Custom CSS Styling

Custom CSS is added using tags$head() and tags$style() to enhance the visual layout, improve user experience, and maintain branding consistency. Key elements styled include:

  • Fixed header and sidebar

  • Custom theme colors

  • Slider and dropdown component styling

  • Improved KPI box formatting

# CSS code for styling the layout of the dashboard
tags$head(
      tags$style(HTML("
    /* FIXED HEADER + LOGO */
    .skin-blue .main-header .navbar {
      background-color: #1A237E !important;
      position: fixed;
      top: 0;
      height: 50px;
      width: 100%;
      z-index: 1030;
      margin-bottom: 0;
    }
    .skin-blue .main-header .logo {
      background-color: #1A237E !important;
      color: white !important;
      position: fixed;
      padding: 10px 15px;
      height: 50px;
      z-index: 1040;
      display: flex;
      align-items: center;
      justify-content: flex-start;
      margin-bottom: 0;
    }
    .skin-blue .main-header .logo:hover {
      background-color: #0D1B5F !important;
    }
    
    /* NAVBAR ITEMS */
    .skin-blue .main-header .navbar .sidebar-toggle,
    .skin-blue .main-header .navbar .navbar-custom-menu > .navbar-nav > li > a {
      background-color: #1A237E !important;
      color: white !important;
    }
    .skin-blue .main-header .navbar .sidebar-toggle:hover,
    .skin-blue .main-header .navbar .navbar-custom-menu > .navbar-nav > li > a:hover {
      background-color: #0D1B5F !important;
    }
    
    /* ACTIVE SIDEBAR MENU HIGHLIGHT */
    .skin-blue .main-sidebar .sidebar-menu > li.active > a {
      border-left: 3px solid #1565C0 !important;
    }
    
    /* SLIDER + DROPDOWN STYLES */
    .irs-bar {
      background-color: #1565C0 !important;
      border-top: 1px solid #1565C0 !important;
      border-bottom: 1px solid #1565C0 !important;
    }
    .irs-slider {
      background: white !important;
      border: 1px solid #1565C0 !important;
    }
    .irs-from, .irs-to, .irs-single {
      background: #1565C0 !important;
      color: white !important;
    }
    .selectize-dropdown .active {
      background-color: #1565C0 !important;
      color: white !important;
    }
    
    /* STICKY SIDEBAR */
    .main-sidebar {
      position: fixed;
      top: 50px;
      bottom: 0;
      overflow-y: auto !important;
      padding-top: 0;
      margin-top: 0;
    }
    
     /* MAKE SIDEBAR CONTENT SCROLLABLE */
    .main-sidebar .sidebar {
      height: 100%;
      overflow-y: auto !important;
      padding-bottom: 240px;
    }
    
    /* CONTENT PADDING ADJUSTMENT */   
    .content-wrapper, .right-side {
      margin-left: 230px;
      padding-top: 50px !important;
      margin-top: 0 !important;
    }
    
    /* TABLE COLUMN STYLE */
    abbr[title] {
      text-decoration: none;
      border-bottom: none;
      cursor: default;
    }
    
    /* KPIs BOXES ADJUSTMENT */
    .small-box {
      position: relative;
      overflow: hidden;
    }
    .small-box .inner h3 {
      font-size: 20px !important;
      font-weight: bold !important;
      margin: 0;
    }
    .small-box .inner p {
      font-size: 16px !important;
      font-weight: normal !important;
      margin: 0;
    }
    .custom-icon {
      font-size: 60px;
      position: absolute;
      top: 50%;
      right: 22px;
      transform: translateY(-112%);
    }
    .custom-icon2 {
      font-size: 60px;
      position: absolute;
      top: 50%;
      right: 15px;
      transform: translateY(-112%);
    }
  "))
  )

Server Logic

This section defines the backend functionality of the dashboard. All server-side logic is encapsulated within the server function, which dynamically responds to user inputs, performs necessary calculations, and renders outputs such as prediction results, charts, and tables.

Each subsection corresponds to a specific tab or component in the dashboard interface. For clarity, the server function is conceptually structured as shown below:

# The complete server logic
server = function(input, output) {
  
  # Tab 1 Inputs: What-If Simulation
  # Tab 2 Inputs: Scenario Comparison
  # Tab 3 Inputs: Analysis Results
}

1. Tab 1 Inputs

The following subsections defines and controls the input components and their behavior for the first tab of the dashboard. Based on user-specified inputs, the app dynamically renders the appropriate controls, displays model predictions with 95% confidence intervals, and presents a contextual reference table.

A. Dynamic Simulation Inputs

The inputs displayed here change based on the selected prediction mode. For example, if users choose to predict wages, they will be prompted to enter their own CPI, inflation rate, GDP, and GDP per capita values. If GDP or CPI-Inflation is selected instead, the relevant predictor inputs will appear accordingly.

# Render dynamic inputs for simulation
  output$dynamic_inputs = renderUI({
    # Common UI across all prediction modes: state selector
    common_ui = list(
      selectInput("selected_state1", "Select State", choices = unique(merged_df$state),
                  selected = "Malaysia")
    )
    
    # Inputs required to predict mean and median wages
    wage_ui = list(
      sliderInput("cpi", "Consumer Price Index (CPI)", min = 50, max = 200, value = 130.4, step = 0.1),
      sliderInput("inflation", "Inflation Rate (%)", min = -15, max = 25, value = 2.5, step = 0.1),
      numericInput("gdp", "GDP (MYR Million)", value = 1568000),
      numericInput("gdp_capita", "GDP per capita (MYR Million)", value = 46900)
    )
    
    # Inputs required to predict GDP and GDP per capita
    gdp_ui = list(
      sliderInput("cpi", "Consumer Price Index (CPI)", min = 50, max = 200, value = 130.4, step = 0.1),
      sliderInput("inflation", "Inflation Rate (%)", min = -15, max = 25, value = 2.5, step = 0.1),
      numericInput("mean_wages", "Mean Wages (MYR)", value = 3440),
      numericInput("median_wages", "Median Wages (MYR)", value = 2600)
    )
    
    # Inputs required to predict CPI and Inflation
    price_ui = list(
      numericInput("gdp", "GDP (MYR Million)", value = 1568000),
      numericInput("gdp_capita", "GDP per capita (MYR Million)", value = 46900),
      numericInput("mean_wages", "Mean Wages (MYR)", value = 3440),
      numericInput("median_wages", "Median Wages (MYR)", value = 2600)
    )
    
    # Display the relevant inputs based on prediction mode
    if (input$predict_mode == "p_wage") {
      tagList(common_ui, wage_ui)
    } else if (input$predict_mode == "p_gdp") {
      tagList(common_ui, gdp_ui)
    } else {
      tagList(common_ui, price_ui)
    }
  })

B. Dynamic Simulation Results

This part of the code generates predicted values using the appropriate Multiple Linear Regression model based on user inputs. Once users click the Run Simulation button, the dashboard displays the predicted values, along with their 95% confidence intervals, in the value boxes.

# Reactive prediction triggered by simulation button
  observeEvent(input$run_sim, {
    # Convert selected state to factor to match model format
    state_input = factor(input$selected_state1, levels = levels(merged_df$state))
    
    if (input$predict_mode == "p_wage") {
      # Prepare input data for wage prediction
      df = data.frame(cpi = input$cpi, inflation = input$inflation, 
                      gdp = input$gdp, gdp_capita = input$gdp_capita, 
                      state = state_input)
      
      # Predict mean and median wages
      pred_mean = predict(mean_model, df, interval = "confidence", level = 0.95)
      pred_median = predict(median_model, df, interval = "confidence", level = 0.95)
      
      # Display predicted mean wage
      output$predictionBox1 = renderValueBox({
        valueBox(paste0("RM ", format(round(pred_mean[1], 2), big.mark = ",")),
                 HTML(paste0(
                   "Predicted Mean Wages in ", state_input,
                   "<br>(95% CI: RM ", format(round(pred_mean[2], 2), big.mark = ","),
                   " - RM ", format(round(pred_mean[3], 2), big.mark = ","), ")")),
                 icon = icon("dollar-sign", class = "custom-icon"),
                 color = "yellow")
      })
      
      # Display predicted median wage
      output$predictionBox2 = renderValueBox({
        valueBox(paste0("RM ", format(round(pred_median[1], 2), big.mark = ",")), 
                 HTML(paste0(
                   "Predicted Median Wages in ", state_input,
                   "<br>(95% CI: RM ", format(round(pred_median[2], 2), big.mark = ","),
                   " - RM ", format(round(pred_median[3], 2), big.mark = ","), ")")),
                 icon = icon("dollar-sign", class = "custom-icon"),
                 color = "blue")
      })
      
    } else if (input$predict_mode == "p_gdp") {
      # Prepare input data for GDP prediction
      df = data.frame(cpi = input$cpi, inflation = input$inflation, state = state_input)
      
      # Predict GDP and GDP per capita
      pred_gdp = predict(gdp_model, df, interval = "confidence", level = 0.95)
      pred_capita = predict(capita_model, df, interval = "confidence", level = 0.95)
      
      # Display predicted GDP
      output$predictionBox1 = renderValueBox({
        valueBox(paste0("RM ", format(round(pred_gdp[1], 2), big.mark = ","), " Million"), 
                 HTML(paste0(
                   "Predicted GDP in ", state_input,
                   "<br>(95% CI: RM ", format(round(pred_gdp[2], 2), big.mark = ","),
                   " Million - RM ", format(round(pred_gdp[3], 2), big.mark = ","), " Million)")),
                 icon = icon("landmark", class = "custom-icon"),
                 color = "yellow")
      })
      
      # Display predicted GDP per capita
      output$predictionBox2 = renderValueBox({ 
        valueBox(paste0("RM ", format(round(pred_capita[1], 2), big.mark = ","), " Million"),
                 HTML(paste0(
                   "Predicted GDP per capita in ", state_input,
                   "<br>(95% CI: RM ", format(round(pred_capita[2], 2), big.mark = ","),
                   " Million - RM ", format(round(pred_capita[3], 2), big.mark = ","), " Million)")),
                 icon = icon("users", class = "custom-icon2"),
                 color = "blue")
      })
      
    } else if (input$predict_mode == "p_price") {
      # Prepare input data for CPI and inflation prediction
      df = data.frame(gdp = input$gdp, gdp_capita = input$gdp_capita, state = state_input)
      
      # Predict CPI and inflation
      pred_cpi = predict(cpi_model, df, interval = "confidence", level = 0.95)
      pred_inflation = predict(inf_model, df, interval = "confidence", level = 0.95)
      
      # Display predicted CPI
      output$predictionBox1 = renderValueBox({
        valueBox(round(pred_cpi[1], 2), 
                 HTML(paste0(
                   "Predicted CPI in ", state_input,
                   "<br>(95% CI: ", format(round(pred_cpi[2], 2), big.mark = ","),
                   " - ", format(round(pred_cpi[3], 2), big.mark = ","), ")")),
                 icon = icon("tags", class = "custom-icon"),
                 color = "yellow")
      })
      
      # Display predicted inflation
      output$predictionBox2 = renderValueBox({
        valueBox(paste0(round(pred_inflation[1], 2), "%"), 
                 HTML(paste0(
                   "Predicted Inflation Rate in ", state_input,
                   "<br>(95% CI: ", format(round(pred_inflation[2], 2), big.mark = ","),
                   "% - ", format(round(pred_inflation[3], 2), big.mark = ","), "%)")),
                 icon = icon("line-chart", class = "custom-icon"),
                 color = "blue")
      })
    }
  })

C. Dynamic Reference Table

The following code renders a reference data table that displays historical records based on the selected prediction mode and state, serving as a guide for first-time users. The table’s content adapts accordingly, providing extra context for the predicted values.

# Render a reference data table based on selected state and prediction mode
  output$ref_table1 = renderDT({
    req(input$predict_mode, input$selected_state1)
    
    # Select data and columns based on prediction mode
    rt1 =  if (input$predict_mode == "p_wage") {
      ref1_df %>% 
        filter(state == input$selected_state1) %>%
        select(
          "Year" = year, "State" = state, 
          "CPI" = cpi, "Inflation" = inflation,
          "GDP" = gdp, "GDP/capita" = gdp_capita)
      
    } else if (input$predict_mode == "p_gdp") {
      ref3_df %>% 
        filter(state == input$selected_state1) %>%
        select(
          "Year" = year, "State" = state, 
          "CPI" = cpi, "Inflation" = inflation,
          "Mean Wages" = mean_wages, "Median Wages" = median_wages)
      
    } else {
      ref2_df %>% 
        filter(state == input$selected_state1) %>%
        select(
          "Year" = year, "State" = state, 
          "GDP" = gdp, "GDP/capita" = gdp_capita,
          "Mean Wages" = mean_wages, "Median Wages" = median_wages)
    }
    
    # Tooltip labels using HTML <abbr> for additional info on hover
    tooltips = switch(input$predict_mode,
                      "p_wage" = c(
                        as.character(tags$abbr(title = "Year", "Year")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Consumer Price Index", "CPI")),
                        as.character(tags$abbr(title = "Inflation Rate (%)", "Inflation")),
                        as.character(tags$abbr(title = "Gross Domestic Product (MYR Million)", "GDP")),
                        as.character(tags$abbr(title = "Gross Domestic Product per capita (MYR Million)",
                                                       "GDP/capita"))
                        ),
                      
                      "p_gdp" = c(
                        as.character(tags$abbr(title = "Year", "Year")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Consumer Price Index", "CPI")),
                        as.character(tags$abbr(title = "Inflation Rate (%)", "Inflation")),
                        as.character(tags$abbr(title = "Mean Wages (MYR)", "Mean Wages")),
                        as.character(tags$abbr(title = "Median Wages (MYR)", "Median Wages"))
                        ),
                      
                      c(
                        as.character(tags$abbr(title = "Year", "Year")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Gross Domestic Product (MYR Million)", "GDP")),
                        as.character(tags$abbr(title = "Gross Domestic Product per capita (MYR Million)",
                                                       "GDP/capita")),
                        as.character(tags$abbr(title = "Mean Wages (MYR)", "Mean Wages")),
                        as.character(tags$abbr(title = "Median Wages (MYR)", "Median Wages"))
                        )
    )
    
    # Render table with appropriate column headers and horizontal scrolling
    datatable(rt1, options = list(scrollX = TRUE), escape = FALSE, colnames = tooltips)
  })

2. Tab 2 Inputs

Next, these subsections manage the user inputs, scenario creation, and comparison table outputs for the second tab of the dashboard. Users can define and save multiple hypothetical economic scenarios by adjusting key indicators and view predicted outcomes side by side.

A. Dynamic Scenario Inputs

Once again, the inputs displayed here change based on the selected scenario mode. It allows users to input their own key indicators values, select a state, and save the scenario for later comparison.

# Components needed to create a dynamic scenario inputs

  # Reactive storage to store multiple user-defined scenarios
  scenarios = reactiveValues(data = list())
  
  # Render dynamic inputs for scenario comparison
  output$scenario_inputs = renderUI({
    # Common UI across all scenario modes: state selector
    common_ui = list(
      selectInput("selected_state2", "Select State", choices = unique(merged_df$state),
                  selected = "Malaysia")
    )
    
    # Inputs required to predict mean and median wages
    wage_ui = list(
      sliderInput("cpi", "Consumer Price Index (CPI)", min = 50, max = 200, value = 130.4, step = 0.1),
      sliderInput("inflation", "Inflation Rate (%)", min = -15, max = 25, value = 2.5, step = 0.1),
      numericInput("gdp", "GDP (MYR Million)", value = 1568000),
      numericInput("gdp_capita", "GDP per capita (MYR Million)", value = 46900)
    )
    
    # Inputs required to predict GDP and GDP per capita
    gdp_ui = list(
      sliderInput("cpi", "Consumer Price Index (CPI)", min = 50, max = 200, value = 130.4, step = 0.1),
      sliderInput("inflation", "Inflation Rate (%)", min = -15, max = 25, value = 2.5, step = 0.1),
      numericInput("mean_wages", "Mean Wages (MYR)", value = 3440),
      numericInput("median_wages", "Median Wages (MYR)", value = 2600)
    )
    
    # Inputs required to predict CPI and Inflation
    price_ui = list(
      numericInput("gdp", "GDP (MYR Million)", value = 1568000),
      numericInput("gdp_capita", "GDP per capita (MYR Million)", value = 46900),
      numericInput("mean_wages", "Mean Wages (MYR)", value = 3440),
      numericInput("median_wages", "Median Wages (MYR)", value = 2600)
    )
    
    # Display the relevant inputs based on scenario mode
    if (input$scenario_mode == "p_wage") {
      tagList(common_ui, wage_ui)
    } else if (input$scenario_mode == "p_gdp") {
      tagList(common_ui, gdp_ui)
    } else {
      tagList(common_ui, price_ui)
    }
  })
  
  # Save user-defined scenario and run prediction based on input values
  observeEvent(input$save_scenario, {
    req(input$scenario_name)
    
    scenario = list(state = input$selected_state2)
    
    selected_mode = input$scenario_mode
    
    state_input = factor(input$selected_state2, levels = levels(merged_df$state))
    
    if (selected_mode == "p_wage") {
      scenario$cpi = input$cpi
      scenario$inflation = input$inflation
      scenario$gdp = input$gdp
      scenario$gdp_capita = input$gdp_capita
      
      df = data.frame(cpi = input$cpi, inflation = input$inflation, 
                      gdp = input$gdp, gdp_capita = input$gdp_capita, 
                      state = state_input)
      scenario$pred_mean = round(predict(mean_model, df), 2)
      scenario$pred_median = round(predict(median_model, df), 2)
      
    } else if (selected_mode == "p_price") {
      scenario$gdp = input$gdp
      scenario$gdp_capita = input$gdp_capita
      scenario$mean_wages = input$mean_wages
      scenario$median_wages = input$median_wages
      
      df = data.frame(gdp = input$gdp, gdp_capita = input$gdp_capita, state = state_input)
      scenario$pred_cpi = round(predict(cpi_model, df), 2)
      scenario$pred_inflation = round(predict(inf_model, df), 2)
      
    } else if (selected_mode == "p_gdp") {
      scenario$cpi = input$cpi
      scenario$inflation = input$inflation
      scenario$mean_wages = input$mean_wages
      scenario$median_wages = input$median_wages
      
      df = data.frame(cpi = input$cpi, inflation = input$inflation, state = state_input)
      scenario$pred_gdp = round(predict(gdp_model, df), 2)
      scenario$pred_capita = round(predict(capita_model, df), 2)
    }
    
    # Save scenario under user-defined name
    scenarios$data[[input$scenario_name]] = scenario
    
    # Notify user of successful save
    showNotification(paste("Scenario", input$scenario_name, "successfully saved"), type = "message")
  })
  
  # Allow user to select one or more saved scenarios for comparison
  output$scenario_selector_ui = renderUI({
    if (length(scenarios$data) > 0) {
      selectInput("selected_scenarios", "Select Scenarios to Compare",
                  choices = names(scenarios$data), multiple = TRUE)}
  })

B. Scenario Comparison Table

This part of the code renders a table comparing key inputs and predicted outputs across user-defined scenarios. The table adapts based on the active scenario mode and includes formatted tooltips for better interpretation.

# Render the Scenario Comparison Table
  output$comp_table = renderDT({
    req(input$selected_scenarios)
    
    selected = input$selected_scenarios
    if (length(selected) == 0) return(NULL)
    
    # Combine selected scenarios into one data frame
    scenario_df = do.call(rbind, lapply(scenarios$data[selected], as.data.frame))
    scenario_df = data.frame(Scenario = selected, scenario_df,
                             check.names = FALSE, row.names = NULL)
    
    # Mapping of column codes to display names
    col_names = c(
      Scenario = "Scenario",
      state = "State",
      cpi = "CPI",
      inflation = "Inflation",
      gdp = "GDP",
      gdp_capita = "GDP/capita",
      mean_wages = "Mean Wages",
      median_wages = "Median Wages",
      pred_mean = "Pred Mean Wages",
      pred_median = "Pred Median Wages",
      pred_cpi = "Pred CPI",
      pred_inflation = "Pred Inflation",
      pred_gdp = "Pred GDP",
      pred_capita = "Pred GDP/capita")
    
    scenario_df = scenario_df %>%
      mutate(across(any_of(c("gdp", "gdp_capita", "mean_wages", "median_wages", 
                             "pred_mean", "pred_median", "pred_gdp", "pred_capita")), 
                    ~ scales::comma(.x, accuracy = 0.01))) %>%
      select(any_of(names(col_names))) %>%
      rename_with(~ col_names[.x], .cols = everything())
    
    # Dynamically assign column tooltips based on scenario mode
    tooltips = switch(input$scenario_mode,
                      "p_wage" = c(
                        as.character(tags$abbr(title = "Scenario Name", "Scenario")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Consumer Price Index", "CPI")),
                        as.character(tags$abbr(title = "Inflation Rate (%)", "Inflation")),
                        as.character(tags$abbr(title = "Gross Domestic Product (MYR Million)", "GDP")),
                        as.character(tags$abbr(title = "Gross Domestic Product per capita (MYR Million)",
                                                       "GDP/capita")),
                        as.character(tags$abbr(title = "Predicted Mean Wages (MYR)",
                                                       "Pred Mean Wages")),
                        as.character(tags$abbr(title = "Predicted Median Wages (MYR)",
                                                       "Pred Median Wages"))
                      ),
                      "p_gdp" = c(
                        as.character(tags$abbr(title = "Scenario Name", "Scenario")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Consumer Price Index", "CPI")),
                        as.character(tags$abbr(title = "Inflation Rate (%)", "Inflation")),
                        as.character(tags$abbr(title = "Mean Wages (MYR)", "Mean Wages")),
                        as.character(tags$abbr(title = "Median Wages (MYR)", "Median Wages")),
                        as.character(tags$abbr(title = "Predicted Gross Domestic Product (MYR Million)",
                                                       "Pred GDP")),
                        as.character(tags$abbr(title = "Predicted Gross Domestic Product 
                                                        per capita (MYR Million)",
                                                       "Pred GDP/capita"))
                      ),
                      c(
                        as.character(tags$abbr(title = "Scenario Name", "Scenario")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Gross Domestic Product (MYR Million)", "GDP")),
                        as.character(tags$abbr(title = "Gross Domestic Product per capita (MYR Million)",
                                                       "GDP/capita")),
                        as.character(tags$abbr(title = "Mean Wages (MYR)", "Mean Wages")),
                        as.character(tags$abbr(title = "Median Wages (MYR)", "Median Wages")),
                        as.character(tags$abbr(title = "Predicted Consumer Price Index", "Pred CPI")),
                        as.character(tags$abbr(title = "Predicted Inflation Rate (%)", "Pred Inflation"))
                      )
    )
    
    # Render comparison table with highlighted predicted output columns
    datatable(scenario_df, options = list(pageLength = 6, scrollX = TRUE),
              escape = FALSE, colnames = tooltips) %>%
      formatStyle(columns = tail(colnames(scenario_df), 2),
                  backgroundColor = "#FFF3CD", fontWeight = "bold")
  })

C. Dynamic Reference Table

The following code renders a reference data table that displays historical records based on the selected scenario mode and state, serving as a guide for first-time users. The table’s content adapts accordingly, providing extra context for the hypotheticals scenarios.

# Render the reference table based on selected state and scenario mode
output$ref_table2 = renderDT({
    req(input$scenario_mode, input$selected_state2)
    
    # Select data and columns based on scenario mode
    rt2 = if (input$scenario_mode == "p_wage") {
      ref1_df %>% 
        filter(state == input$selected_state2) %>%
        select(
          "Year" = year, "State" = state, 
          "CPI" = cpi, "Inflation" = inflation,
          "GDP" = gdp, "GDP/capita" = gdp_capita)
      
    } else if (input$scenario_mode == "p_gdp") {
      ref3_df %>% 
        filter(state == input$selected_state2) %>%
        select(
          "Year" = year, "State" = state, 
          "CPI" = cpi, "Inflation" = inflation,
          "Mean Wages" = mean_wages, "Median Wages" = median_wages)
      
    } else {
      ref2_df %>% 
        filter(state == input$selected_state2) %>%
        select(
          "Year" = year, "State" = state, 
          "GDP" = gdp, "GDP/capita" = gdp_capita,
          "Mean Wages" = mean_wages, "Median Wages" = median_wages)
    }
    
    # Tooltip labels using HTML <abbr> for additional info on hover
    tooltips = switch(input$scenario_mode,
                      "p_wage" = c(
                        as.character(tags$abbr(title = "Year", "Year")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Consumer Price Index", "CPI")),
                        as.character(tags$abbr(title = "Inflation Rate (%)", "Inflation")),
                        as.character(tags$abbr(title = "Gross Domestic Product (MYR Million)", "GDP")),
                        as.character(tags$abbr(title = "Gross Domestic Product per capita (MYR Million)",
                                                       "GDP/capita"))
                      ),
                      "p_gdp" = c(
                        as.character(tags$abbr(title = "Year", "Year")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Consumer Price Index", "CPI")),
                        as.character(tags$abbr(title = "Inflation Rate (%)", "Inflation")),
                        as.character(tags$abbr(title = "Mean Wages (MYR)", "Mean Wages")),
                        as.character(tags$abbr(title = "Median Wages (MYR)", "Median Wages"))
                      ),
                      c(
                        as.character(tags$abbr(title = "Year", "Year")),
                        as.character(tags$abbr(title = "State Name", "State")),
                        as.character(tags$abbr(title = "Gross Domestic Product (MYR Million)", "GDP")),
                        as.character(tags$abbr(title = "Gross Domestic Product per capita (MYR Million)", 
                                                       "GDP/capita")),
                        as.character(tags$abbr(title = "Mean Wages (MYR)", "Mean Wages")),
                        as.character(tags$abbr(title = "Median Wages (MYR)", "Median Wages"))
                      )
    )
    
    # Render table with appropriate column headers and horizontal scrolling
    datatable(rt2, options = list(scrollX = TRUE), escape = FALSE, colnames = tooltips)
  })

3. Tab 3 Inputs

Finally, these concluding subsections provide a comprehensive overview of the results from several analyses included in the dashboard. The results are presented through various visualizations, such as line charts, bar charts, heatmaps, and tables for easy interpretation.

A. Full Data Table

The full data table displays the merged data set containing CPI, inflation rate, GDP, GDP per capita, and wage indicators (mean and median) by state and year. It serves as a reference for users to explore the raw values before or after running any simulations.

# Render a comprehensive merged data table for all states and years
  output$table = renderDT({
    # Select and rename columns for clarity and readability
    merged_df = merged_df %>% 
      select(
        "Year" = year,
        "State" = state,
        "Mean Wages" = mean_wages,
        "Median Wages" = median_wages,
        "CPI" = cpi,
        "Inflation Rate" = inflation,
        "GDP" = gdp,
        "GDP per capita" = gdp_capita)
    
    # Tooltip labels using HTML <abbr> for additional info on hover
    tooltips = c(
      as.character(tags$abbr(title = "Year", "Year")),
      as.character(tags$abbr(title = "State Name", "State")),
      as.character(tags$abbr(title = "Mean Wages (MYR)", "Mean Wages")),
      as.character(tags$abbr(title = "Median Wages (MYR)", "Median Wages")),
      as.character(tags$abbr(title = "Consumer Price Index", "CPI")),
      as.character(tags$abbr(title = "Inflation Rate (%)", "Inflation Rate")),
      as.character(tags$abbr(title = "Gross Domestic Product (MYR Million)", "GDP")),
      as.character(tags$abbr(title = "Gross Domestic Product per capita (MYR Million)", 
                                     "GDP per capita"))
    )
    
    # Display the interactive table with horizontal scroll and hover labels
    datatable(merged_df, options = list(pageLength = 9, scrollX = TRUE),
              escape = FALSE, colnames = tooltips)
  })

B. Descriptive Analysis

This section offers exploratory insights through dynamic visualization and filtering. Users can interactively select a time range and state to observe historical trends and relationships between economic indicators. These tools provide intuitive understanding of underlying data patterns before diving into predictions or simulations.

# Reactive filter: filter data set based on selected year range using slider
  filtered_data = reactive({
    req(input$year_range)
    merged_df %>% 
      filter(year >= input$year_range[1], year <= input$year_range[2])
  })

The line chart below dynamically visualizes the trend of a selected economic indicator over time for a chosen state. The shaded area and interactive points help highlight patterns and outliers across the filtered years.

# Render line chart over time for selected state and indicator
  output$trend_plot = renderPlotly({
    req(input$selected_state3, input$selected_variable)
    
    # Filter data based on selected state and year range
    trend_data = filtered_data() %>% 
      filter(state == input$selected_state3)
    
    selected_var = input$selected_variable
    min_y = min(trend_data[[selected_var]], na.rm = TRUE)
    max_y = max(trend_data[[selected_var]], na.rm = TRUE)
    
    # Dynamic plot title based on selected state and indicator
    title = variable_labels[[selected_var]]$title
    y_title = variable_labels[[selected_var]]$y_title
    plot_title = paste(title, "Trend over Time for", input$selected_state3)
    
    # Create ggplot line chart
    trend_line = ggplot(trend_data, aes(x = year)) +
      geom_line(aes_string(y = selected_var), color = "#0073C2FF", linewidth = 1.2) +
      geom_point(aes_string(y = selected_var), color = "#EFC000FF", size = 2) +
      geom_ribbon(aes_string(ymin = min_y, ymax = selected_var), fill = "#0073C2FF", alpha = 0.3) +
      geom_hline(yintercept = 0, color = "black", linetype = "solid", linewidth = 0.27) +
      labs(title = plot_title, x = "Year", y = y_title) +
      theme_economist_white(base_size = 14) +
      theme(plot.title = element_text(size = 12.70, hjust = 0)) +
      scale_x_continuous(
        breaks = seq(min(trend_data$year), max(trend_data$year), by = 1),
        labels = as.integer(seq(min(trend_data$year), max(trend_data$year), by = 1))) +
      scale_y_continuous(
        limits = c(min_y, max_y), labels = scales::comma)
    
    # Convert ggplot to interactive Plotly chart
    plotly_trend_plot = ggplotly(trend_line) %>%
      layout(title = list(text = plot_title, 
                          font = list(size = 17, family = "Arial Black", color = "black"),
                          x = 0, xanchor = "left", y = 0.95, yanchor = "top"))
    
    return(plotly_trend_plot)
  })

The correlation heatmap reveals pairwise relationships between key economic indicators within the selected state and year range. Users can quickly identify strong positive or negative relationships through color intensity.

# Render correlation heatmap for selected state and year range
  output$correlation_heatmap = renderPlotly({
    req(input$selected_state3)
    
    # Filter numeric data for the selected state
    numeric_data = filtered_data() %>% 
      filter(state == input$selected_state3) %>%
      select(mean_wages, median_wages, cpi, inflation, gdp, gdp_capita)
    
    # Compute correlation matrix and convert to long format
    cor_matrix = round(cor(numeric_data, use = "complete.obs"), 2)
    cor_long = as.data.frame(as.table(cor_matrix))
    colnames(cor_long) = c("Var1", "Var2", "Correlation")
    
    # Rename variables for readability in the plot
    cor_long$Var1 = recode(cor_long$Var1, mean_wages = "Mean Wages", median_wages = "Median Wages",
                           cpi = "CPI", inflation = "Inflation Rate", gdp = "GDP", 
                           gdp_capita = "GDP per Capita")
    
    cor_long$Var2 = recode(cor_long$Var2, mean_wages = "Mean Wages", median_wages = "Median Wages",
                           cpi = "CPI", inflation = "Inflation Rate", gdp = "GDP", 
                           gdp_capita = "GDP per Capita")
    
    # Create ggplot heatmap
    heatmap_plot = ggplot(cor_long, 
                          aes(Var1, Var2, fill = Correlation, text = paste0("Corr: ", Correlation))) +
      geom_tile(color = "white") +
      scale_fill_gradient2(low = "springgreen", mid = "white", high = "tomato", midpoint = 0,
                           limit = c(-1,1), name = "Correlation", breaks = seq(-1, 1, 0.5),
                           labels = c("-1.0", "-0.5", "   0", " 0.5", " 1.0")) +
      labs(title = paste("Economic Indicators Correlation Heatmap for", input$selected_state3, "from",
                         input$year_range[1], "to", input$year_range[2])) +
      theme_economist_white(base_size = 14) +
      theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1),
            axis.title = element_blank(), 
            plot.title = element_text(size = 12.70, hjust = 0)) +
      theme(
        legend.position = "right", legend.title = element_text(size = 12),
        legend.text = element_text(size = 9), legend.key.width = unit(0.5, "cm"),
        legend.key.height = unit(2, "cm"))
    
    # Convert ggplot to Plotly and handle insufficient data properly
    plotly_heatmap = ggplotly(heatmap_plot, tooltip = "text")
    
    if (nrow(numeric_data) < 2) {
      plotly_heatmap = plotly_heatmap %>%
        layout(title = list(text = paste0("Not Enough Data to Compute the<br>Correlation for ", 
                                          input$selected_state3, " in ", input$year_range[1]), 
                            font = list(size = 17, family = "Arial Black", color = "black"), 
                            x = 0.5, xanchor = "center", y = 0.95, yanchor = "top"), 
               autosize = TRUE)
      
    } else {
      plotly_heatmap = plotly_heatmap %>%
        layout(title = list(text = paste0("Economic Indicators Correlation Heatmap<br>for ", 
                                          input$selected_state3, " from ", 
                                          input$year_range[1], " to ", input$year_range[2]), 
                            font = list(size = 17, family = "Arial Black", color = "black"), 
                            x = 0.5, xanchor = "center", y = 0.95, yanchor = "top"), 
               autosize = TRUE)
    }
    
    return(plotly_heatmap)
  })

C. What-If Analysis

This section presents a visual summary of the What-If Simulation results defined in Tab 1. It allows users to see how certain predicted indicator outcomes would vary across all Malaysian states based on a hypothetical set of economic inputs. This enables quick comparison and interpretation of how different states may be affected under the same simulated scenario.

This interactive bar chart displays the predicted values of the selected indicator for each state. It allows users to easily identify the states with the highest and lowest projected values under the defined input scenario.

# Render predicted bar chart by state
  output$bar_plot = renderPlotly({
    req(input$predict_mode, input$selected_variable, input$run_sim)
    
    # Filter out national level and set state factor levels
    states = unique(merged_df$state)
    states = states[states != "Malaysia"]
    states = droplevels(states)
    
    # Assign relevant predictors and model based on prediction mode
    prediction_data = data.frame(state = factor(states, levels = levels(merged_df$state)))
    
    if (input$predict_mode == "p_wage") {
      prediction_data$cpi = input$cpi
      prediction_data$inflation = input$inflation
      prediction_data$gdp = input$gdp
      prediction_data$gdp_capita = input$gdp_capita
      model = if (input$selected_variable == "mean_wages") mean_model else median_model
      
    } else if (input$predict_mode == "p_gdp") {
      prediction_data$cpi = input$cpi
      prediction_data$inflation = input$inflation
      model = if (input$selected_variable == "gdp") gdp_model else capita_model
      
    } else {
      prediction_data$gdp = input$gdp
      prediction_data$gdp_capita = input$gdp_capita
      model = if (input$selected_variable == "cpi") cpi_model else inf_model
    }
    
    # Predict the selected economic indicator across states
    prediction_data$prediction = predict(model, newdata = prediction_data)
    
    # Dynamic plot title based on selected indicator
    title = variable_labels[[input$selected_variable]]$title
    y_title = variable_labels[[input$selected_variable]]$y_title
    
    # Set gradient color scheme
    base_colors = c("tomato", "#F0F700", "#66CC33")
    if (input$selected_variable %in% c("cpi", "inflation")) {
      base_colors = rev(base_colors)
    }
    
    # Create ggplot bar chart
    bar_state = ggplot(prediction_data, aes(x = reorder(state, -prediction),
                                            y = prediction, fill = prediction)) +
      geom_bar(stat = "identity") +
      geom_hline(yintercept = 0, color = "black", linetype = "solid", linewidth = 0.3) +
      labs(title = paste("Predicted", title, "by State"), x = "State", y = y_title) +
      scale_fill_gradientn(colors = base_colors) +
      scale_y_continuous(labels = scales::comma) +
      theme_economist_white(base_size = 14) +
      theme(legend.position = "none",
            axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1))
    
    # Convert ggplot to interactive Plotly chart
    ggplotly(bar_state, tooltip = c("label", "x", "y"))
  })

This interactive choropleth map visualizes the predicted values geographically using color gradients. It enables a quick glance at regional disparities and how each state is affected under the simulated economic scenario.

# Render Predicted Choropleth Map of Malaysia
  output$map_plot = renderLeaflet({
    req(input$predict_mode, input$selected_variable, input$run_sim)
    
    # Filter out national level and set state factor levels
    states = unique(merged_df$state)
    states = states[states != "Malaysia"]
    states = droplevels(states)
    
    # Assign relevant predictors and model based on prediction mode
    prediction_data = data.frame(state = factor(states, levels = levels(merged_df$state)))
    
    if (input$predict_mode == "p_wage") {
      prediction_data$cpi = input$cpi
      prediction_data$inflation = input$inflation
      prediction_data$gdp = input$gdp
      prediction_data$gdp_capita = input$gdp_capita
      model = if (input$selected_variable == "mean_wages") mean_model else median_model
      
    } else if (input$predict_mode == "p_gdp") {
      prediction_data$cpi = input$cpi
      prediction_data$inflation = input$inflation
      model = if (input$selected_variable == "gdp") gdp_model else capita_model
      
    } else {
      prediction_data$gdp = input$gdp
      prediction_data$gdp_capita = input$gdp_capita
      model = if (input$selected_variable == "cpi") cpi_model else inf_model
    }
    
    # Predict the selected economic indicator across states
    prediction_data$prediction = predict(model, newdata = prediction_data)
    
    # Load and match state geometry data from Natural Earth
    malaysia_map = ne_states(country = "Malaysia", returnclass = "sf")
    malaysia_map$name = as.character(malaysia_map$name)
    
    # Adjust state naming to match data set
    malaysia_map$name[malaysia_map$name == "Kuala Lumpur"] = "W.P. Kuala Lumpur"
    malaysia_map$name[malaysia_map$name == "Labuan"] = "W.P. Labuan"
    malaysia_map$name[malaysia_map$name == "Putrajaya"] = "W.P. Putrajaya"
    malaysia_map$name = factor(malaysia_map$name, levels = levels(merged_df$state))
    
    # Merge map with predictions
    map_data = left_join(malaysia_map, prediction_data, by = c("name" = "state"))
    
    # Generate color palette for bins
    base_colors = c("#E60000", "#FFED00", "#006633")
    if (input$selected_variable %in% c("cpi", "inflation")) {
      base_colors = rev(base_colors)
    }
    state_colors = colorRampPalette(base_colors)(7)
    pal = colorBin(palette = state_colors, domain = na.omit(map_data$prediction),
                   bins = 7, na.color = "transparent")
    
    # Create interactive choropleth map
    leaflet(data = map_data) %>%
      addProviderTiles("OpenStreetMap") %>%
      addPolygons(
        fillColor = ~pal(prediction), weight = 1, opacity = 1, color = "black",
        dashArray = "3", fillOpacity = 0.7, label = ~paste0(name, ": ", round(prediction, 2)),
        highlightOptions = highlightOptions(
          weight = 2, color = "red", fillOpacity = 0.9, bringToFront = TRUE),
        labelOptions = labelOptions(direction = "auto")) %>%
      addLegend(pal = pal, values = ~prediction, opacity = 0.7,
                title = variable_labels[[input$selected_variable]]$y_title,
                position = "bottomright") %>%
      addControl(html = paste0("<div style='font-size:22px; font-weight:bold;
                        padding:6px; border-radius:6px;'>", "Predicted ",
                        variable_labels[[input$selected_variable]]$title,
                        " Choropleth Map of Malaysia</div>"),
                 position = "topright")
  })

D. Time Series Forecasting

Finally, this section enables users to forecast future values of selected economic indicators for a chosen state. It uses historical annual data and fits an ARIMA model to project trends over a user-defined forecast horizon. The resulting visualization helps anticipate future economic outcomes, based on the assumption that past patterns will continue.

# Components needed to generate forecast using time series

  # Reactive filter to extract time series data for the selected variable and state
  ts_filtered_data = reactive({
    req(input$forecast_variable, input$forecast_state)
    
    merged_df %>%
      filter(state == input$forecast_state) %>%
      select(year, all_of(input$forecast_variable))
  })
  
  # Time Series Forecasting
  observeEvent(input$run_forecast, {
    req(ts_filtered_data(), input$forecast_horizon)
    
    # Convert the filtered variable into a time series object
    ts_data = ts(ts_filtered_data()[[input$forecast_variable]] + 
                   rnorm(length(ts_filtered_data()[[input$forecast_variable]]), 
                         mean = 0, sd = 0.5), 
                 start = min(ts_filtered_data()$year), frequency = 1)
    
    # Fit an ARIMA(2,1,2) model
    fit = arima(ts_data, order = c(2, 1, 2))
    
    # Forecast for user-specified horizon (in years)
    forecast_result = forecast(fit, h = input$forecast_horizon)
    
    # Create a forecast data frame with future years
    forecast_df = data.frame(
      year = seq(max(ts_filtered_data()$year) + 1,
                 max(ts_filtered_data()$year) + input$forecast_horizon, by = 1),
      forecast = forecast_result$mean)
    
    # Render the forecast time series plot using Plotly
    output$forecast_plot = renderPlotly({
      all_years = seq(min(ts_filtered_data()$year), max(forecast_df$year), by = 1) 
      
      plot_ly() %>%
        add_trace(data = ts_filtered_data(), x = ~year, 
                  y = as.formula(paste0("~", input$forecast_variable)), 
                  type = "scatter", mode = "markers+lines", name = "Actual",
                  line = list(color = "#0073C2FF"), marker = list(size = 6)) %>%
        add_trace(data = forecast_df, x = ~year, y = ~forecast, 
                  type = "scatter", mode = "markers+lines", name = "Forecast",
                  line = list(color = "#E60000", dash = 'dot'), marker = list(size = 6)) %>%
        layout(title = list(
          text = paste(variable_labels[[input$forecast_variable]]$title, 
                       "Time Series Forecast for", input$forecast_state),
          font = list(size = 18, family = "Arial Black", color = "#333333"),
          xanchor = "center", y = 0.99),
          xaxis = list(
            title = "Year", tickvals = all_years, ticktext = all_years,
            showline = TRUE, linecolor = "#333333", gridcolor = "#D3D3D3"), 
          yaxis = list(
            title = variable_labels[[input$forecast_variable]]$y_title,
            tickformat = ",", showgrid = TRUE, showline = TRUE,
            linecolor = "#333333", gridcolor = "#D3D3D3"),
          legend = list(
            orientation = "h", xanchor = "center", x = 0.5, y = -0.2),
          shapes = list(
            list(
              type = "rect",
              x0 = min(forecast_df$year), x1 = max(forecast_df$year),
              y0 = 0, y1 = 1, xref = "x", yref = "paper",
              fillcolor = "rgba(230, 0, 0, 0.2)", line = list(width = 0))),
          hovermode = "x unified", paper_bgcolor = "#EBEBEB", plot_bgcolor = "#EBEBEB")
    })
  })

ShinyDashboard Creation

Finally, to bring the dashboard to life, the application is launched using the shinyApp(ui, server) function, which binds together the user interface and server logic into a functional Shiny app.

Following this, the What-If Analysis Dashboard can be deployed online using shinyapps.io. For this project, it has been hosted there for public access. The dashboard enables users to perform interactive simulations, compare hypothetical scenarios, and analyse key economic indicators across Malaysian states. It uses built-in predictive models to generate real-time predictions based on user-defined inputs.

You can access the full live interactive dashboard here:
👉 Launch What-If Analysis Dashboard

Dashboard Preview

The image below provides a static preview of Tab 1 of the What-If Analysis Dashboard interface. To experience full functionality, please use the full-screen link above to access the live version.

Preview for PDF/Offline Readers
Figure 1: Static Preview of Tab 1 — What-If Simulation

ℹ️ Note: This static preview is intended for offline or non-interactive viewing. For full interactivity and the best experience, visit the dashboard link provided above.

Conclusion

This report has outlined the full process of developing the What-If Analysis Dashboard, including data handling, model construction, and dynamic user interface design using Shiny. By integrating interactive features with predictive analytics, the dashboard provides a centralized platform for monitoring Malaysia’s economic performance through key indicators such as CPI, inflation rate, wages, and GDP across all states.

By enabling users to model potential outcomes and evaluate policy impacts under various hypothetical scenarios, the dashboard serves as a powerful decision-support tool for policymakers, analysts, and stakeholders. The final product is publicly accessible online and demonstrates how R and Shiny applications can transform complex data into user-friendly and dynamic dashboards.

Ultimately, this project not only exemplifies the real-world application of statistical knowledge but also contributes a valuable digital resource to support data-driven governance and strategic economic planning in Malaysia. Moving forward, the dashboard can be further enhanced by incorporating more granular data, expanding the range of economic indicators, and integrating real-time data sources to improve its responsiveness and relevance for long-term planning.