1. Environment Setup and Package Loading

packages_to_load_viz <- c(
  "tidyverse", "sf", "rnaturalearth", "rnaturalearthdata", 
  "ggrepel", "patchwork", "RColorBrewer", "viridis", 
  "metafor", "bruceR", "kableExtra", "ggthemes",
  "svglite", 
  "grDevices", 
  "stringr" 
)

for (pkg in packages_to_load_viz) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    install.packages(pkg, dependencies = TRUE, repos = "https://cran.rstudio.com/")
  }
  library(pkg, character.only = TRUE)
}

# IMPORTANT: Ensure this RData file name matches your actual data file.
RDATA_FILE <- "satlife20250508.RData" 
if (file.exists(RDATA_FILE)) {
  load(RDATA_FILE)
  print(paste("Workspace '", RDATA_FILE, "' loaded successfully.", sep=""))
} else {
  stop(paste("Error:", RDATA_FILE, "not found. Please run the main analysis script first to generate it."))
}
## [1] "Workspace 'satlife20250508.RData' loaded successfully."
# Use constants defined in setup chunk
FIGURE_WIDTH_MM <- FIGURE_WIDTH_MM_CONST 
MM_TO_INCHES_FUNC_CONST <- 1 / 25.4 

theme_publication <- function(base_size = 11, base_family = "sans") {
  (ggthemes::theme_foundation(base_size = base_size, base_family = base_family) 
   + theme(
      plot.title = element_text(face="bold", hjust=0.5, size=rel(1.1)), 
      plot.subtitle = element_blank(), 
      text = element_text(family = base_family, color = "black"), 
      panel.background = element_rect(colour = NA, fill = "white"), 
      plot.background = element_rect(colour = NA, fill = "white"),
      panel.border = element_rect(colour = "black", fill = NA, linewidth = 0.5), 
      axis.title = element_text(face = "bold", size = rel(1.0), color = "black"), 
      axis.title.y = element_text(angle = 90, vjust = 2),
      axis.title.x = element_text(vjust = -0.2),
      axis.text = element_text(size = rel(0.9), color = "black"), 
      axis.line = element_line(colour = "black", linewidth = 0.5), 
      axis.ticks = element_line(colour = "black", linewidth = 0.5), 
      panel.grid.major = element_line(colour = "#f0f0f0"), 
      panel.grid.minor = element_blank(),
      legend.key = element_rect(fill = "white", colour = "grey80"),
      legend.position = "none", 
      legend.background = element_rect(fill="transparent"), 
      legend.box.background = element_blank(), 
      legend.title = element_text(face = "italic", size=rel(0.9)),
      legend.text = element_text(size=rel(0.8)),
      plot.margin = unit(c(3, 3, 3, 3), "mm"), 
      strip.background = element_rect(colour = "#f0f0f0", fill = "#f0f0f0"), 
      strip.text = element_text(face = "bold", size=rel(0.9))
    ))
}

theme_set(theme_publication()) 
palette_discrete_viridis <- viridis::viridis_pal(option = "D")(5) 

# 修改后的保存函数 - 根据期刊要求使用EPS和TIFF格式
save_journal_figure <- function(plot_object, 
                               filename_base, 
                               width_mm = FIGURE_WIDTH_MM, 
                               height_mm, 
                               is_ggplot = TRUE,
                               base_plot_draw_func = NULL,
                               is_bitmap = FALSE) { 
  
  if (!dir.exists("figures")) {
    dir.create("figures")
  }

  width_in <- width_mm * MM_TO_INCHES_FUNC_CONST 
  height_in <- height_mm * MM_TO_INCHES_FUNC_CONST 
  
  # --- Vector Format (EPS for journal submission) ---
  if (!is_bitmap) {
    eps_path <- file.path("figures", paste0(filename_base, ".eps"))
    tryCatch({
      if (is_ggplot) {
        # 对于ggplot2对象,使用ggsave保存EPS
        ggsave(filename = eps_path, plot = plot_object, 
               device = cairo_ps, # cairo_ps可以处理透明度和复杂图形
               width = width_in, height = height_in, 
               dpi = 300, # 虽是矢量格式,但设置DPI以防有嵌入的栅格元素
               units = "in")
      } else {
        # 对于基础图形,使用设备函数
        postscript(file = eps_path, width = width_in, height = height_in, 
                   horizontal = FALSE, onefile = FALSE, 
                   paper = "special", family = "sans", 
                   encoding = "default")
        
        if (!is.null(base_plot_draw_func)) {
          par(family = "sans")
          base_plot_draw_func() 
        } else { 
          print(plot_object) # 简单基础图形的备用方案
        }
        dev.off()
      }
      cat(paste("Saved EPS:", eps_path, "(W:", width_mm, "mm, H:", height_mm, "mm)\n"))
    }, error = function(e) {
      cat(paste("Failed to save EPS", eps_path, ":", e$message, "\n"))
      # 确保即使出错也关闭设备
      current_dev <- grDevices::dev.cur()
      if (current_dev > 1) { # 检查设备是否打开
         grDevices::dev.off(which = current_dev)
      }
    })
  } else {
    # --- 位图格式 (TIFF for journal submission) ---
    tiff_path <- file.path("figures", paste0(filename_base, ".tiff"))
    tryCatch({
      if (is_ggplot) {
        ggsave(filename = tiff_path, plot = plot_object, 
               device = "tiff", 
               width = width_in, height = height_in, 
               dpi = 300, # 期刊要求300 DPI
               compression = "lzw", # 使用无损压缩
               units = "in")
      } else {
        tiff(filename = tiff_path, width = width_mm, height = height_mm, 
             units = "mm", res = 300, compression = "lzw")
        
        if (!is.null(base_plot_draw_func)) {
          par(family = "sans")
          base_plot_draw_func() 
        } else { 
          print(plot_object)
        }
        dev.off()
      }
      cat(paste("Saved TIFF:", tiff_path, "(W:", width_mm, "mm, H:", height_mm, "mm)\n"))
    }, error = function(e) {
      cat(paste("Failed to save TIFF", tiff_path, ":", e$message, "\n"))
      current_dev <- grDevices::dev.cur()
      if (current_dev > 1) {
         grDevices::dev.off(which = current_dev)
      }
    })
  }
  
  # --- PNG (For HTML preview) ---
  png_path <- file.path("figures", paste0(filename_base, ".png"))
  tryCatch({
    if (is_ggplot) {
      ggsave(filename = png_path, plot = plot_object, 
             device = "png", 
             width = width_in, height = height_in, 
             dpi = 300, 
             units = "in")
    } else {
      png(filename = png_path, width = width_mm, height = height_mm, 
          units = "mm", res = 300, bg = "white")
      
      if (!is.null(base_plot_draw_func)) {
          par(family = "sans") 
          base_plot_draw_func() 
      } else { 
          print(plot_object)
      }
      dev.off()
    }
    cat(paste("Saved PNG:", png_path, "(W:", width_mm, "mm, H:", height_mm, "mm)\n"))
    
    # 返回PNG路径用于HTML输出中显示
    return(png_path)
    
  }, error = function(e) {
    cat(paste("Failed to save PNG", png_path, ":", e$message, "\n"))
    current_dev <- grDevices::dev.cur()
    if (current_dev > 1) {
       grDevices::dev.off(which = current_dev)
    }
    return(NULL)
  })
}

country_name_overrides <- c("CHN" = "Mainland China")
get_mapped_country_name <- function(coun_code_val) {
  # Function body remains the same...
  if (coun_code_val %in% names(country_name_overrides)) { return(country_name_overrides[coun_code_val]) }
  if (exists("country_level_data") && coun_code_val %in% country_level_data$coun) {
    if ("country" %in% names(country_level_data)) {
      country_name_from_meta <- country_level_data$country[country_level_data$coun == coun_code_val]
      if (length(country_name_from_meta) == 1 && !is.na(country_name_from_meta) && nzchar(trimws(as.character(country_name_from_meta)))) {
        return(as.character(country_name_from_meta))
      }
    }
  }
  return(as.character(coun_code_val))
}

2. Main Text Figures

Figure 1: Conceptual Model and Data Flow

Figure 1: Conceptual framework of income-life satisfaction relationship across countries. This figure illustrates the theoretical pathways connecting individual income and life satisfaction, highlighting potential cultural and economic moderators that might influence this relationship across different national contexts.

Note: This figure is to be created externally as a vector graphic (180mm wide) and inserted into the manuscript. The conceptual framework integrates elements from economic theory (e.g., diminishing marginal utility), social comparison models, and cross-cultural psychology perspectives to explain how income-satisfaction associations might vary globally.

cat("Figure 1: Conceptual Model - To be created externally (180mm wide) and inserted into the manuscript.\n")
## Figure 1: Conceptual Model - To be created externally (180mm wide) and inserted into the manuscript.

Figure 2: Geographical Distribution of Average Life Satisfaction

Figure 2: Global distribution of average life satisfaction scores by country. This choropleth map displays mean life satisfaction scores (0-10 scale) for all countries included in the analysis, with darker blue indicating higher levels of reported life satisfaction.

Note: Data were aggregated from multiple longitudinal aging studies including CHARLS (China), ELSA (England), HRS (USA), KLOSA (Korea), MHAS (Mexico), and SHARE (Europe). Country-level averages were calculated using harmonized life satisfaction measures from individual responses collected between 2015 and 2023. Variations in average satisfaction demonstrate substantial cross-national differences that may be partially explained by cultural and economic factors examined in subsequent analyses.

# Calculate height in mm for saving function
fig2_height_mm_calc <- FIGURE_WIDTH_MM * FIG2_HEIGHT_FACTOR 

# --- Data Preparation (same as before) ---
avg_satlife_fig2 <- clean_data %>% filter(!is.na(coun) & !is.na(satlife)) %>% group_by(coun) %>% summarise(mean_satlife = mean(satlife, na.rm = TRUE), .groups = 'drop') %>% filter(!is.na(mean_satlife))
world_map_sf_fig2_raw <- rnaturalearth::ne_countries(scale = "medium", returnclass = "sf") %>% select(iso_a3, name_en = name, sovereignt, geometry, area_proxy = scalerank) %>% filter(!is.na(iso_a3)) %>% sf::st_make_valid()
wintri_proj <- "+proj=wintri +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
world_map_projected_fig2 <- world_map_sf_fig2_raw %>% sf::st_transform(crs = wintri_proj)
graticules_sf <- sf::st_graticule(ndiscr = 100, lat = seq(-90, 90, 15), lon = seq(-180, 180, 30)) %>% sf::st_transform(crs = wintri_proj)
world_map_joined_fig2 <- world_map_projected_fig2 %>% left_join(avg_satlife_fig2 %>% rename(mean_satlife_data = mean_satlife), by = c("iso_a3" = "coun")) 
label_data_candidates_sf <- world_map_joined_fig2 %>% filter(!is.na(mean_satlife_data))
manual_label_iso <- c("CHN", "USA", "MEX", "KOR", "IND", "RUS", "JPN", "CAN", "GBR", "DEU", "FRA", "ESP", "ITA") 
label_data_manual_sf <- label_data_candidates_sf %>% filter(iso_a3 %in% manual_label_iso)
label_data_repel_sf <- label_data_candidates_sf %>% filter(!iso_a3 %in% manual_label_iso, area_proxy <= 5) 
label_data_manual_coords <- data.frame() 
if(nrow(label_data_manual_sf) > 0) { centroids_manual <- sf::st_point_on_surface(label_data_manual_sf$geometry); temp_coords <- data.frame(iso_a3 = label_data_manual_sf$iso_a3, st_coordinates(centroids_manual)) %>% filter(!is.na(X) & !is.na(Y)); kor_offset_x <- 700000; kor_offset_y <- 150000; if ("KOR" %in% temp_coords$iso_a3) { temp_coords <- temp_coords %>% mutate(X = ifelse(iso_a3 == "KOR", X + kor_offset_x, X), Y = ifelse(iso_a3 == "KOR", Y + kor_offset_y, Y)) }; label_data_manual_coords <- temp_coords }
label_data_repel_coords <- data.frame() 
if(nrow(label_data_repel_sf) > 0) { centroids_repel <- sf::st_point_on_surface(label_data_repel_sf$geometry); label_data_repel_coords <- data.frame(iso_a3 = label_data_repel_sf$iso_a3, st_coordinates(centroids_repel)) %>% filter(!is.na(X) & !is.na(Y)) }
min_lat_crop <- 0; max_lat_crop <- 85
full_xlim_proj <- st_bbox(world_map_projected_fig2)[c("xmin", "xmax")]
y_min_transformed <- st_sfc(st_point(c(0, min_lat_crop)), crs=4326) %>% st_transform(wintri_proj) %>% st_coordinates() %>% .[, "Y"]
y_max_transformed <- st_sfc(st_point(c(0, max_lat_crop)), crs=4326) %>% st_transform(wintri_proj) %>% st_coordinates() %>% .[, "Y"]
# --- Plot Creation (same as before, but using Blues palette) ---
plot_fig2 <- ggplot() +
    geom_sf(data = graticules_sf, color = "gray85", linewidth = 0.15, linetype = "dotted") +
    geom_sf(data = world_map_joined_fig2, aes(fill = mean_satlife_data), color = "gray50", linewidth = 0.12) +
    { if(nrow(label_data_manual_coords) > 0) { geom_text(data = label_data_manual_coords, aes(x = X, y = Y, label = iso_a3), size = 2.0, color = "black", fontface = "bold", check_overlap = FALSE) }} +
    { if(nrow(label_data_repel_coords) > 0) { ggrepel::geom_text_repel(data = label_data_repel_coords, aes(x = X, y = Y, label = iso_a3), size = 1.8, color = "black", fontface = "bold", min.segment.length = 0.1, box.padding = unit(0.15, "lines"), point.padding = unit(0.1, "lines"), segment.color = "gray30", segment.size = 0.20, force = 2.0, force_pull = 0.5, max.overlaps = Inf, bg.color = "white", bg.r = 0.08) }} +
    scale_fill_distiller(palette = "Blues", direction = 1, name = "Mean Life Satisfaction (0-10)", limits = c(0, 10), na.value = "gray92", guide = guide_colorbar(barwidth = unit(FIGURE_WIDTH_MM * 0.30, "mm"), barheight = unit(3.0, "mm"), title.position = "top", title.hjust = 0.5, ticks.colour = "gray20", ticks.linewidth = 0.25, nbin=10)) + 
    coord_sf(crs = wintri_proj, xlim = full_xlim_proj, ylim = c(y_min_transformed, y_max_transformed), expand = FALSE, datum = NA) +
    theme_void(base_family = "sans", base_size = 10) + 
    theme(legend.position = "bottom", legend.direction = "horizontal", legend.background = element_rect(fill="white", colour = NA), legend.title = element_text(size = rel(0.8), color = "black", face = "bold"), legend.text = element_text(size = rel(0.7), color = "black"), plot.background = element_rect(fill = "white", colour = NA), plot.margin = margin(t=1, r=1, b=1, l=1, unit="mm"))

# --- Save and Display ---
if (nrow(avg_satlife_fig2) > 0) { 
  # Save both EPS (for journal) and PNG (for HTML preview)
  png_path <- save_journal_figure(plot_object = plot_fig2, 
                                 filename_base = "Fig2_Country_Life_Satisfaction", 
                                 height_mm = fig2_height_mm_calc, 
                                 width_mm = FIGURE_WIDTH_MM, 
                                 is_bitmap = FALSE)  # 使用EPS格式,因为这是线图
  # Display PNG in HTML output
  knitr::include_graphics(png_path)
} else {
  cat("Figure 2: No valid life satisfaction data found to display.\n") 
  plot_fig2_no_data <- ggplot() + theme_void() + annotate("text", x=0, y=0, label="Figure 2: No data available") + theme_publication(); print(plot_fig2_no_data)
}
## Saved EPS: figures/Fig2_Country_Life_Satisfaction.eps (W: 180 mm, H: 99 mm)
## Saved PNG: figures/Fig2_Country_Life_Satisfaction.png (W: 180 mm, H: 99 mm)

Figure 3: Meta-Analysis of Income-Life Satisfaction Associations

Figure 3: Forest plot of standardized income-life satisfaction effect sizes across countries. This meta-analysis displays standardized beta coefficients showing the relationship between income and life satisfaction for each analyzed country, along with 95% confidence intervals and an overall pooled estimate.

Note: Beta coefficients represent standardized effect sizes from regression models adjusting for age, gender, health status, and partnership status. GDP values are per-capita in international dollars (PPP adjusted). ATK represents the Atkinson inequality index (higher values indicate greater inequality). Sample size with analytical weight is shown for each country. The pooled estimate was derived using a random-effects model. Most countries show significant positive associations between income and life satisfaction, though with substantial cross-national heterogeneity in effect size magnitude.

# Check for required objects from the loaded RData file
if (!exists("model_meta_base") || !is.null(model_meta_base) && inherits(model_meta_base, "try-error")) {
  cat("Error: 'model_meta_base' object for meta-analysis not found or is an error object. Forest plot cannot be generated.\n")
  plot_fig3_placeholder <- ggplot() + theme_void() + annotate("text", x=0, y=0, label="Figure 3: Data missing (model_meta_base)")
  print(plot_fig3_placeholder)
} else if (!exists("country_coefs_std_meta")) {
  cat("Error: 'country_coefs_std_meta' (country-level coefficients for meta-analysis) not found. Forest plot cannot be generated.\n")
  plot_fig3_placeholder <- ggplot() + theme_void() + annotate("text", x=0, y=0, label="Figure 3: Data missing (country_coefs_std_meta)")
  print(plot_fig3_placeholder)
} else if (!exists("country_specific_models_output")) {
  cat("Error: 'country_specific_models_output' (for model types) not found. Forest plot cannot be generated.\n")
  plot_fig3_placeholder <- ggplot() + theme_void() + annotate("text", x=0, y=0, label="Figure 3: Data missing (country_specific_models_output)")
  print(plot_fig3_placeholder)
} else {
  
  # Prepare data for the forest plot
  # Using 'country_coefs_std_meta' as the base, as it's loaded from RData
  data_for_forest <- country_coefs_std_meta %>%
    # Join model_type information
    left_join(
      country_specific_models_output %>% select(coun, model_type_from_source = model_type),
      by = "coun"
    ) %>%
    mutate(
      # Use the 'coun' variable directly for study labels in the plot.
      study_label = coun, 
      model_type = ifelse(!is.na(model_type_from_source), as.character(model_type_from_source), "N/A")
    ) # Removed arrange(estimate) to align with order="obs" from new snippet

  # Calculate weights for display (as percentage)
  # This uses 'w_perc_display' consistent with previous logic in satlife_visualizations_fix
  data_for_forest$w_perc_display <- "" 
  w_percentage_values <- tryCatch(
    weights(model_meta_base, type = "summary"), 
    error = function(e) {
      warning("Could not retrieve weights from model_meta_base. N-based proportions will be used for display if N is available.")
      return(NULL)
    }
  )
  if (!is.null(w_percentage_values) && length(w_percentage_values) == nrow(data_for_forest)) {
    data_for_forest$w_perc_display <- sprintf("%.1f", w_percentage_values)
  } else if ("n_obs" %in% names(data_for_forest) && sum(data_for_forest$n_obs, na.rm = TRUE) > 0) {
    total_n_obs <- sum(data_for_forest$n_obs, na.rm = TRUE)
    if (total_n_obs > 0) {
        data_for_forest$w_perc_display <- sprintf("%.1f", (data_for_forest$n_obs / total_n_obs) * 100)
    }
  }
  # Use 'w_perc_display' for the text
  n_weight_text_display <- paste0(
    data_for_forest$n_obs,
    ifelse(data_for_forest$w_perc_display != "" & !is.na(data_for_forest$w_perc_display),
           paste0(" (", data_for_forest$w_perc_display, "%)"), # Using w_perc_display
           "")
  )
  
  # Prepare ilab dataframe based on the new snippet's columns
  ilab_vars <- data.frame(
    GDP = ifelse(!is.na(data_for_forest$gdp), round(data_for_forest$gdp, 0), "N/A"),
    ATK = ifelse(!is.na(data_for_forest$atk), formatC(data_for_forest$atk, digits = 2, format = "f"), "N/A"),
    N_Weight = n_weight_text_display, # Using the calculated n_weight_text_display
    stringsAsFactors = FALSE
  )
  # Set column name for N_Weight as per new snippet (without '%')
  colnames(ilab_vars) <- c("GDP", "ATK", "N (Weight)") 

  # ilab positions from the new snippet
  ilab_xpos <- c(-0.9, -0.7, -0.55) 

  # Dynamically calculate plot height
  num_studies_fig3 <- nrow(data_for_forest)
  # Adjusted spacing based on new snippet's implicit needs (fewer ilab, potentially different cex)
  header_footer_space_mm <- 50 
  mm_per_study <- 6 # Adjusted from 5.5, can be tuned      
  fig3_height_mm <- header_footer_space_mm + (num_studies_fig3 * mm_per_study)
  fig3_height_in <- mm_to_inches(fig3_height_mm)
  fig3_width_in <- mm_to_inches(FIGURE_WIDTH_MM)

  # Function to draw the forest plot, adapted from new snippet and existing structure
  draw_forest_plot_fig3 <- function() {
      par(family = "sans", mar=c(5,1,1.5,1)) # Margins from new snippet

      summary_model <- summary(model_meta_base) # Keep for p-value check
      summary_diamond_label <- "Overall Pooled Estimate" # Default label
      if (model_meta_base$method == "FE") {
        summary_diamond_label <- "Fixed-Effects Model"
      } else if (model_meta_base$method %in% c("REML", "DL", "HE", "HS", "SJ", "PM", "EB")) {
        summary_diamond_label <- "Random-Effects Model"
      }

      metafor::forest(model_meta_base, # Use the model from satlife_visualizations_fix
               slab = data_for_forest$study_label, # 'coun' codes
               ilab = ilab_vars,               
               ilab.xpos = ilab_xpos,
               ilab.pos = 4, 
               ilab.lab = colnames(ilab_vars), # Use ilab.lab from new snippet
               xlim = c(-1.4, 0.8),         # From new snippet
               alim = c(-0.1, 0.24),        # From new snippet
               refline = 0,
               addpred = TRUE,              # Maintained from satlife_visualizations_fix
               predstyle = "bar",           # From new snippet
               pred.lab = summary_diamond_label, # Kept for clarity
               showweights = FALSE, 
               digits = 3, 
               order = "obs",               # From new snippet (data_for_forest not sorted by estimate)
               psize = 1.5,                 # From new snippet
               efac = c(1, 1),              # From new snippet
               cex = 0.8,                   # From new snippet
               cex.lab = 1,                 # From new snippet
               cex.axis = 0.85,             # From new snippet
               xlab = "Standardized Effect Size", # From new snippet
               header = c("Country", "Effect [95% CI]"), 
               col = "darkblue", 
               border = "darkblue", 
               colout = "black",            # From new snippet
               shade = "zebra" 
      )
      # Manual text for ilab headers is removed as ilab.lab is used.
      
      # Corrected if condition for significance note (though mtext is commented out)
      if (!is.null(summary_model$pval) && 
          length(summary_model$pval) >= 1 && # Check if pval exists and has at least one element
          !is.na(summary_model$pval[1]) &&   # Check if the first p-value is not NA
          summary_model$pval[1] < 0.05) {
         # Example mtext if re-enabled:
         # mtext(paste0("Note: ", summary_diamond_label, " is statistically significant (p = ", 
         #              format.pval(summary_model$pval[1], digits=3, eps=0.001), ")."), 
         #       side=1, line=3.5, adj=0, cex=0.8*0.9) # Adjusted cex based on new base cex
      }
  }

  # 对图3使用EPS格式(期刊要求的矢量格式)
  eps_file_fig3 <- file.path("figures", "Fig3_Meta_Forest_Plot.eps")
  postscript(file = eps_file_fig3, width = fig3_width_in, height = fig3_height_in, 
            horizontal = FALSE, paper = "special", family = "sans")
  draw_forest_plot_fig3()
  dev.off()
  cat(paste("Saved forest plot as EPS:", eps_file_fig3, "\n"))

  # 同时保存PNG用于HTML预览
  png_file_fig3 <- file.path("figures", "Fig3_Meta_Forest_Plot.png")
  png(png_file_fig3, width = FIGURE_WIDTH_MM, height = fig3_height_mm, units = "mm", res = 300, bg = "white")
  draw_forest_plot_fig3()
  dev.off() 
  cat(paste("Saved forest plot as PNG:", png_file_fig3, "\n"))
  
  # Display PNG in HTML output
  knitr::include_graphics(png_file_fig3)
}
## Saved forest plot as EPS: figures/Fig3_Meta_Forest_Plot.eps
## Saved forest plot as PNG: figures/Fig3_Meta_Forest_Plot.png
Forest plot of income-life satisfaction associations.

Forest plot of income-life satisfaction associations.

Figure 4: Correlation Matrix of Country-Level Variables

Figure 4: Correlation matrix of key country-level economic and cultural indicators. This matrix displays Pearson correlation coefficients between national-level variables used in moderator analyses, with scatter plots and fitted lines in the upper triangle and correlation values in the lower triangle.

Note: GNI and GDP represent per-capita values (PPP-adjusted international dollars). Atk (Atkinson index) and GINI are inequality measures. Cultural dimensions include: PDI (Power Distance), IDV (Individualism), MAS (Masculinity), UAI (Uncertainty Avoidance), LTO (Long-Term Orientation), and IVR (Indulgence vs. Restraint) from Hofstede’s cultural framework. Strong positive correlations exist between GDP and GNI (r = 0.95), and between IDV and GDP/GNI (r = 0.72/0.74), while strong negative correlations exist between inequality metrics and wealth indicators. Cultural dimensions show moderate intercorrelations, supporting their use as distinct moderators in subsequent analyses.

# Calculate height in mm for saving function
fig4_height_mm_calc <- FIGURE_WIDTH_MM * FIG4_HEIGHT_FACTOR

# --- Data Checks ---
if (!exists("country_level_desc_stats_data") || is.null(country_level_desc_stats_data) || nrow(country_level_desc_stats_data) == 0) {
    cat("Data for Fig 4 (country_level_desc_stats_data) not found or empty.\n")
    plot_fig4_placeholder <- ggplot() + theme_void() + annotate("text", x=0, y=0, label="Fig 4: Data missing") + theme_publication()
    print(plot_fig4_placeholder)
} else {
    # --- Data Preparation ---
    fig4_filename_base <- "Fig4_Country_Level_Corr"
    formal_var_names_fig4 <- c(gni = "GNI", gdp = "GDP", atk = "Atk", gini = "GINI", pdi = "PDI", idv = "IDV", mas = "MAS", uai = "UAI", lto = "LTO", ivr = "IVR")
    data_for_corr_plot_renamed <- country_level_desc_stats_data %>% select(any_of(names(formal_var_names_fig4))) %>% rename_with(~ formal_var_names_fig4[.], .cols = any_of(names(formal_var_names_fig4)))

    # --- Drawing Function ---
    draw_describe_plot_func <- function() {
      # No need to set par(family="sans") here, save_journal_figure handles it for base plots
      bruceR::Describe(data_for_corr_plot_renamed, all.as.numeric = TRUE, digits = 2, plot = TRUE, upper.triangle = TRUE, upper.smooth = "lm")
    }
    
    # --- Save and Display ---
    # 使用EPS格式保存(矢量图,期刊要求)
    png_path <- save_journal_figure(plot_object = NULL, 
                                   filename_base = fig4_filename_base, 
                                   width_mm = FIGURE_WIDTH_MM, 
                                   height_mm = fig4_height_mm_calc, 
                                   is_ggplot = FALSE, 
                                   base_plot_draw_func = draw_describe_plot_func,
                                   is_bitmap = FALSE)  # 使用EPS格式
    
    # Display PNG in HTML output
    if (!is.null(png_path)) {
      knitr::include_graphics(png_path)
    } else {
      cat("Warning: PNG file for Fig 4 could not be created.\n")
    }
}
## Descriptive Statistics:
## ──────────────────────────────────────────────────────────────────────────
##        N     Mean       SD |   Median      Min       Max Skewness Kurtosis
## ──────────────────────────────────────────────────────────────────────────
## GNI   33 36392.12 13540.29 | 34920.00 13140.00  69430.00     0.58    -0.30
## GDP   33 37862.91 17267.38 | 35220.98 13201.67 107888.58     1.97     5.76
## Atk   33     0.18     0.06 |     0.17     0.11      0.36     0.91     0.39
## GINI  33     0.33     0.05 |     0.33     0.25      0.48     0.72     0.66
## PDI   33    51.85    21.28 |    50.00    11.00    100.00     0.14    -0.55
## IDV   33    56.42    20.22 |    60.00    18.00     91.00    -0.37    -0.99
## MAS   33    47.73    23.00 |    47.00     5.00    100.00     0.05    -0.63
## UAI   33    70.76    21.90 |    74.00    23.00    112.00    -0.44    -0.52
## LTO   33    57.64    18.76 |    58.00    24.00    100.00     0.16    -0.70
## IVR   33    46.45    21.90 |    48.00    13.00     97.00     0.20    -1.06
## ──────────────────────────────────────────────────────────────────────────
## Saved EPS: figures/Fig4_Country_Level_Corr.eps (W: 180 mm, H: 153 mm)
## Descriptive Statistics:
## ──────────────────────────────────────────────────────────────────────────
##        N     Mean       SD |   Median      Min       Max Skewness Kurtosis
## ──────────────────────────────────────────────────────────────────────────
## GNI   33 36392.12 13540.29 | 34920.00 13140.00  69430.00     0.58    -0.30
## GDP   33 37862.91 17267.38 | 35220.98 13201.67 107888.58     1.97     5.76
## Atk   33     0.18     0.06 |     0.17     0.11      0.36     0.91     0.39
## GINI  33     0.33     0.05 |     0.33     0.25      0.48     0.72     0.66
## PDI   33    51.85    21.28 |    50.00    11.00    100.00     0.14    -0.55
## IDV   33    56.42    20.22 |    60.00    18.00     91.00    -0.37    -0.99
## MAS   33    47.73    23.00 |    47.00     5.00    100.00     0.05    -0.63
## UAI   33    70.76    21.90 |    74.00    23.00    112.00    -0.44    -0.52
## LTO   33    57.64    18.76 |    58.00    24.00    100.00     0.16    -0.70
## IVR   33    46.45    21.90 |    48.00    13.00     97.00     0.20    -1.06
## ──────────────────────────────────────────────────────────────────────────
## Saved PNG: figures/Fig4_Country_Level_Corr.png (W: 180 mm, H: 153 mm)

Figure 5: Moderation by Economic and Inequality Indicators

Figure 5: Economic and inequality indicators as moderators of income-life satisfaction associations. These scatter plots show how country-level economic factors modify the relationship between income and life satisfaction, with each point representing a country’s standardized effect size and the size of points proportional to precision (1/SE).

Note: All moderators are standardized (z-scores). Panels show: (A) GDP per capita, (B) GNI per capita, (C) Atkinson inequality index, and (D) GINI coefficient. Regression lines show fitted meta-regression models weighted by effect size precision. Countries with higher GDP and GNI per capita show significantly stronger positive associations between income and life satisfaction (p < 0.01). Inequality indicators (Atkinson index and GINI) demonstrate a negative moderation effect, with higher inequality associated with weaker income-satisfaction relationships, suggesting that the psychological benefits of income may be diminished in more unequal societies.

# Calculate height in mm for saving function
fig5_height_mm_calc <- FIGURE_WIDTH_MM * FIG5_HEIGHT_FACTOR

# --- Data Checks ---
if (!exists("country_coefs_std_meta")) {
    cat("Data for Fig 5 (country_coefs_std_meta) not found.\n")
    plot_A <- ggplot() + theme_void() + labs(title="A") + theme_publication()
    plot_B <- ggplot() + theme_void() + labs(title="B") + theme_publication()
    plot_C <- ggplot() + theme_void() + labs(title="C") + theme_publication()
    plot_D <- ggplot() + theme_void() + labs(title="D") + theme_publication()
    plot_fig5_combined <- (plot_A | plot_B) / (plot_C | plot_D)
    print(plot_fig5_combined)
} else {
    # --- Data Preparation & Plotting ---
    moderators_fig5_def <- list("gdp_z" = "A: GDP per Capita (z)", "gni_z" = "B: GNI per Capita (z)", "atk_z" = "C: Atkinson Index (z)", "gini_z" = "D: GINI Coefficient (z)")
    plot_list_fig5 <- list() 
    moderator_data_fig5 <- country_coefs_std_meta %>% mutate(precision_weight = 1 / sqrt(vi))
    for (mod_var_fig5_idx in seq_along(moderators_fig5_def)) {
      mod_var_name_fig5 <- names(moderators_fig5_def)[mod_var_fig5_idx]
      full_mod_title <- moderators_fig5_def[[mod_var_name_fig5]]
      subplot_letter_fig5 <- stringr::str_extract(full_mod_title, "^[A-D]")
      x_axis_title_fig5 <- stringr::str_replace(full_mod_title, "^[A-D]:\\s+", "")
      if (mod_var_name_fig5 %in% names(moderator_data_fig5)) {
        num_distinct_mod_fig5 <- moderator_data_fig5 %>% filter(!is.na(.data[[mod_var_name_fig5]])) %>% pull(.data[[mod_var_name_fig5]]) %>% n_distinct()
        p_fig5_panel <- ggplot(moderator_data_fig5, aes_string(x = mod_var_name_fig5, y = "estimate")) + 
          geom_point(aes(size = precision_weight), alpha = 0.6, color = palette_discrete_viridis[1]) + 
          ggrepel::geom_text_repel(aes(label = coun), size = 2.2, max.overlaps = Inf, segment.alpha = 0.5, segment.color = "grey40", min.segment.length = 0, box.padding = 0.25, point.padding = 0.2, bg.color = "white", bg.r = 0.05) + 
          scale_size_continuous(guide = "none") + 
          labs(title = subplot_letter_fig5, x = x_axis_title_fig5, y = if(mod_var_fig5_idx %% 2 == 1) expression("Country Effect Size ("*beta*")") else "") + 
          theme_publication() 
        if (num_distinct_mod_fig5 >= 2) { 
          p_fig5_panel <- p_fig5_panel + geom_smooth(method = "lm", aes(weight = precision_weight), color = "black", se = TRUE, linewidth=0.7) 
        }
        plot_list_fig5[[mod_var_name_fig5]] <- p_fig5_panel
      } else { 
        plot_list_fig5[[mod_var_name_fig5]] <- ggplot() + annotate("text",x=0,y=0,label=paste(mod_var_name_fig5, "\nnot found.")) + theme_void() + labs(title=subplot_letter_fig5, x=x_axis_title_fig5) + theme_publication() 
      }
    }
    # --- Combine and Save ---
    if (length(plot_list_fig5) == 4) {
      plot_fig5_combined <- wrap_plots(plot_list_fig5, ncol = 2) 
      # 使用EPS格式保存(矢量图,期刊要求)
      png_path <- save_journal_figure(plot_fig5_combined, 
                                     "Fig5_Eco_Ineq_Moderators", 
                                     height_mm = fig5_height_mm_calc, 
                                     width_mm = FIGURE_WIDTH_MM,
                                     is_bitmap = FALSE)  # 使用EPS格式
      
      # Display PNG in HTML output
      if (!is.null(png_path)) {
        knitr::include_graphics(png_path)
      } else {
        cat("Warning: PNG file for Fig 5 could not be created.\n")
      }
    } else {
      cat("Could not generate all 4 panels for Figure 5.\n")
      plot_fig5_combined_error <- ggplot() + theme_void() + labs(title="Figure 5 Error") + theme_publication()
      print(plot_fig5_combined_error)
    }
}
## Saved EPS: figures/Fig5_Eco_Ineq_Moderators.eps (W: 180 mm, H: 171 mm)
## Saved PNG: figures/Fig5_Eco_Ineq_Moderators.png (W: 180 mm, H: 171 mm)

Figure 6: Moderation by Cultural Dimensions

Figure 6: Hofstede’s cultural dimensions as moderators of income-life satisfaction associations. These scatter plots illustrate how each of Hofstede’s six cultural dimensions modifies the relationship between income and life satisfaction, with each point representing a country’s standardized effect size and point size proportional to precision (1/SE).

Note: All cultural dimensions are standardized (z-scores). Panels show: (A) Power Distance (PDI), (B) Individualism (IDV), (C) Masculinity (MAS), (D) Uncertainty Avoidance (UAI), (E) Long-Term Orientation (LTO), and (F) Indulgence vs. Restraint (IVR). Regression lines show fitted meta-regression models weighted by effect size precision. Most notably, individualism (IDV) shows a significant positive moderation effect (p < 0.01), with more individualistic societies exhibiting stronger relationships between income and life satisfaction. Power distance (PDI) shows a significant negative moderation effect (p < 0.05), suggesting that hierarchical societies may experience decreased psychological benefits from personal income. Other cultural dimensions show non-significant moderation trends that warrant further investigation with larger country samples.

# Calculate height in mm for saving function
fig6_height_mm_calc <- FIGURE_WIDTH_MM * FIG6_HEIGHT_FACTOR

# --- Data Checks ---
if (!exists("country_coefs_std_meta")) {
    cat("Data for Fig 6 (country_coefs_std_meta) not found.\n")
    plot_list_fig6_init <- replicate(6, ggplot() + theme_void() + labs(title="Data Missing") + theme_publication(), simplify = FALSE)
    plot_fig6_combined <- wrap_plots(plot_list_fig6_init, ncol=2)
    print(plot_fig6_combined)
} else {
    # --- Data Preparation & Plotting ---
    moderators_fig6_def <- list("pdi_z" = "A: Power Distance (PDI, z)", "idv_z" = "B: Individualism (IDV, z)", "mas_z" = "C: Masculinity (MAS, z)", "uai_z" = "D: Uncertainty Avoidance (UAI, z)", "lto_z" = "E: Long-Term Orientation (LTO, z)", "ivr_z" = "F: Indulgence vs. Restraint (IVR, z)")
    plot_list_fig6 <- list() 
    moderator_data_fig6 <- country_coefs_std_meta %>% mutate(precision_weight = 1 / sqrt(vi))
    for (i in seq_along(moderators_fig6_def)) {
      mod_var_name_fig6 <- names(moderators_fig6_def)[i]
      full_mod_title_fig6 <- moderators_fig6_def[[mod_var_name_fig6]]
      subplot_letter_fig6 <- stringr::str_extract(full_mod_title_fig6, "^[A-F]")
      x_axis_title_fig6 <- stringr::str_replace(full_mod_title_fig6, "^[A-F]:\\s+", "")
      if (mod_var_name_fig6 %in% names(moderator_data_fig6)) {
        num_distinct_mod_fig6 <- moderator_data_fig6 %>% filter(!is.na(.data[[mod_var_name_fig6]])) %>% pull(.data[[mod_var_name_fig6]]) %>% n_distinct()
        p_fig6_panel <- ggplot(moderator_data_fig6, aes_string(x = mod_var_name_fig6, y = "estimate")) + 
          geom_point(aes(size = precision_weight), alpha = 0.6, color = viridis::viridis_pal(option="plasma")(8)[i+1]) + 
          ggrepel::geom_text_repel(aes(label = coun), size = 2.2, max.overlaps = Inf, segment.alpha = 0.5, segment.color = "grey50", min.segment.length = 0, box.padding = 0.25, point.padding = 0.2, bg.color = "white", bg.r = 0.05) + 
          scale_size_continuous(guide = "none") + 
          labs(title = subplot_letter_fig6, x = x_axis_title_fig6, y = if(i %% 2 == 1) expression("Country Effect Size ("*beta*")") else "") + 
          theme_publication()
        if (num_distinct_mod_fig6 >= 2) { 
          p_fig6_panel <- p_fig6_panel + geom_smooth(method = "lm", aes(weight = precision_weight), color = "black", se = TRUE, linewidth=0.7) 
        }
        plot_list_fig6[[mod_var_name_fig6]] <- p_fig6_panel
      } else { 
        plot_list_fig6[[mod_var_name_fig6]] <- ggplot() + annotate("text",x=0,y=0,label=paste(mod_var_name_fig6, "\nnot found.")) + theme_void() + labs(title=subplot_letter_fig6, x=x_axis_title_fig6) + theme_publication() 
      }
    }
    # --- Combine and Save ---
    if (length(plot_list_fig6) == 6) {
      plot_fig6_combined <- wrap_plots(plot_list_fig6, ncol = 2, nrow = 3)
      # 使用EPS格式保存(矢量图,期刊要求)
      png_path <- save_journal_figure(plot_fig6_combined, 
                                     "Fig6_Cultural_Moderators", 
                                     height_mm = fig6_height_mm_calc, 
                                     width_mm = FIGURE_WIDTH_MM,
                                     is_bitmap = FALSE)  # 使用EPS格式
      
      # Display PNG in HTML output
      if (!is.null(png_path)) {
        knitr::include_graphics(png_path)
      } else {
        cat("Warning: PNG file for Fig 6 could not be created.\n")
      }
    } else {
      cat("Could not generate all 6 panels for Figure 6.\n")
      plot_fig6_combined_error <- ggplot() + theme_void() + labs(title="Figure 6 Error") + theme_publication()
      print(plot_fig6_combined_error)
    }
}
## Saved EPS: figures/Fig6_Cultural_Moderators.eps (W: 180 mm, H: 243 mm)
## Saved PNG: figures/Fig6_Cultural_Moderators.png (W: 180 mm, H: 243 mm)

3. Session Information

This section provides information about the R environment and package versions used to generate these visualizations, ensuring reproducibility.

sessionInfo()
## R version 4.4.3 (2025-02-28 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26100)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=Chinese (Simplified)_Singapore.utf8 
## [2] LC_CTYPE=Chinese (Simplified)_Singapore.utf8   
## [3] LC_MONETARY=Chinese (Simplified)_Singapore.utf8
## [4] LC_NUMERIC=C                                   
## [5] LC_TIME=Chinese (Simplified)_Singapore.utf8    
## 
## time zone: Asia/Singapore
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] svglite_2.2.0           ggthemes_5.1.0          kableExtra_1.4.0       
##  [4] interactions_1.2.0      lmerTest_3.1-3          lme4_1.1-37            
##  [7] performance_0.13.0      effectsize_1.0.0        emmeans_1.11.1         
## [10] data.table_1.17.0       bruceR_2024.6           metafor_4.8-0          
## [13] numDeriv_2016.8-1.1     metadat_1.4-0           Matrix_1.7-3           
## [16] viridis_0.6.5           viridisLite_0.4.2       RColorBrewer_1.1-3     
## [19] patchwork_1.3.0         ggrepel_0.9.6           rnaturalearthdata_1.0.0
## [22] rnaturalearth_1.0.1     sf_1.0-20               lubridate_1.9.4        
## [25] forcats_1.0.0           stringr_1.5.1           dplyr_1.1.4            
## [28] purrr_1.0.4             readr_2.1.5             tidyr_1.3.1            
## [31] tibble_3.2.1            ggplot2_3.5.2           tidyverse_2.0.0        
## 
## loaded via a namespace (and not attached):
##  [1] mathjaxr_1.8-0      rstudioapi_0.17.1   jsonlite_2.0.0     
##  [4] wk_0.9.4            datawizard_1.0.2    magrittr_2.0.3     
##  [7] TH.data_1.1-3       estimability_1.5.1  farver_2.1.2       
## [10] nloptr_2.2.1        rmarkdown_2.29      ragg_1.4.0         
## [13] vctrs_0.6.5         minqa_1.2.8         terra_1.8-42       
## [16] htmltools_0.5.8.1   broom_1.0.8         s2_1.1.7           
## [19] sass_0.4.10         parallelly_1.44.0   KernSmooth_2.23-26 
## [22] bslib_0.9.0         plyr_1.8.9          sandwich_3.1-1     
## [25] zoo_1.8-14          cachem_1.1.0        lifecycle_1.0.4    
## [28] pkgconfig_2.0.3     R6_2.6.1            fastmap_1.2.0      
## [31] rbibutils_2.3       future_1.40.0       digest_0.6.37      
## [34] GGally_2.2.1        furrr_0.3.1         textshaping_1.0.1  
## [37] labeling_0.4.3      timechange_0.3.0    mgcv_1.9-3         
## [40] httr_1.4.7          compiler_4.4.3      proxy_0.4-27       
## [43] withr_3.0.2         pander_0.6.6        backports_1.5.0    
## [46] DBI_1.2.3           psych_2.5.3         ggstats_0.9.0      
## [49] broom.mixed_0.2.9.6 MASS_7.3-65         classInt_0.4-11    
## [52] tools_4.4.3         units_0.8-7         glue_1.8.0         
## [55] nlme_3.1-168        grid_4.4.3          generics_0.1.3     
## [58] gtable_0.3.6        tzdb_0.5.0          class_7.3-23       
## [61] hms_1.1.3           xml2_1.3.8          pillar_1.10.2      
## [64] splines_4.4.3       lattice_0.22-7      survival_3.8-3     
## [67] tidyselect_1.2.1    knitr_1.50          reformulas_0.4.1   
## [70] gridExtra_2.3       xfun_0.52           stringi_1.8.7      
## [73] yaml_2.3.10         boot_1.3-31         evaluate_1.0.3     
## [76] codetools_0.2-20    cli_3.6.4           xtable_1.8-4       
## [79] parameters_0.25.0   systemfonts_1.2.3   Rdpack_2.6.4       
## [82] jquerylib_0.1.4     dichromat_2.0-0.1   Rcpp_1.0.14        
## [85] globals_0.17.0      coda_0.19-4.1       png_0.1-8          
## [88] parallel_4.4.3      bayestestR_0.15.3   listenv_0.9.1      
## [91] mvtnorm_1.3-3       scales_1.4.0        e1071_1.7-16       
## [94] insight_1.2.0       crayon_1.5.3        rlang_1.1.5        
## [97] mnormt_2.1.1        multcomp_1.4-28     jtools_2.3.0