diff --git a/.gitignore b/.gitignore
index f5faa2b155054d6430c80612eee339aa23c6cf6d..e90c34642f3bb6553479d997d8d1bcf3c93b1f17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,6 +52,7 @@ examples/pydrograph/
 *.html
 beaufortSea/images/*
 !*/images/in/*
+!fishAsFood/in/recfishfoods_families_thermal.xlsx
 # allow empty files to be committed
 !*/in/.empty
 !*/out/.empty
diff --git a/fishAsFood/_targets.R b/fishAsFood/_targets.R
index 360eddda4f7b3e4f309d53efcea5d31a5d6f84b9..d296258fbed394a3bffa18e8aec22941b9794b5a 100644
--- a/fishAsFood/_targets.R
+++ b/fishAsFood/_targets.R
@@ -26,6 +26,11 @@ p1 <- list(
       return(out_file)
     },
     format = 'file'
+  ),
+  tar_target(
+    p2_metadata_xlsx,
+    file.path('in', 'recfishfoods_families_thermal.xlsx'),
+    format = 'file'
   )
 )
 
@@ -42,6 +47,15 @@ p2 <- list(
       out_file = '../public/total_price.json'
     ),
     format = 'file'
+  ),
+  tar_target(
+    p2_climate_csv,
+    build_climate_csv(
+      data = p2_data,
+      metadata_file = p2_metadata_xlsx,
+      out_file = '../public/fish_as_food_climate.csv'
+    ),
+    format = 'file'
   )
 )
 
diff --git a/fishAsFood/in/recfishfoods_families_thermal.xlsx b/fishAsFood/in/recfishfoods_families_thermal.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..9a84bc9f920218d9d7d79f18a10ec5374aa18e68
Binary files /dev/null and b/fishAsFood/in/recfishfoods_families_thermal.xlsx differ
diff --git a/fishAsFood/src/data_utils.R b/fishAsFood/src/data_utils.R
index e134fbd920ff242fd010fa9519ae6474294b83e7..da0aefcad40ff77c9603ca06f62358545afc27b0 100644
--- a/fishAsFood/src/data_utils.R
+++ b/fishAsFood/src/data_utils.R
@@ -6,15 +6,19 @@ clean_input_data <- function(data_file) {
   data |>
     mutate(
       family = ifelse(family == 'Salmonindae', 'Salmonidae', family),
-      species_common = ifelse(species_common == 'Bey?ehir bleak', 
-                              'BeyÅŸehir bleak', 
-                              species_common)
+      species_common = case_when(
+        species_common == 'Bey?ehir bleak' ~ 'BeyÅŸehir bleak',
+        species_taxa_sci == 'Brachyplatystoma rousseauxii' ~ 'Gilded catfish',
+        species_common == 'Speckled pavon, speckled peacock bass (percid)' ~ 
+          'Speckled pavon',
+        TRUE ~ species_common
       )
+    )
 }
 
 build_nested_json <- function(data, focal_columns, out_file) {
   data_list <- data |>
-    filter(!is.na(!!rlang::sym(focal_columns[['value']]))) |>
+    filter(!is.na(focal_columns[['value']])) |>
     group_by(name = family) |>
     group_modify(~ {
       species_list <- .x |> 
@@ -32,5 +36,61 @@ build_nested_json <- function(data, focal_columns, out_file) {
   json_data <- paste0('{\n "name": "fish", "children":', json_data, '\n}')
   write(json_data, file = out_file)
   
+  return(out_file)
+}
+
+build_climate_csv <- function(data, metadata_file, out_file) {
+  # read in metadata
+  metadata <- read_xlsx(metadata_file)
+  
+  # filter out missing and uncertain data
+  data_subset <- data |>
+    filter(!is.na(species_common), !is.na(MCDM_VUL_2030_45)) |>
+    left_join(metadata) |>
+    filter(uncertainty_classification < 4)
+  
+  # get subset of better represented families with >2 species
+  focal_families <- data_subset |>
+    group_by(family, species_common) |>
+    count() |>
+    group_by(family) |>
+    count() |>
+    filter(n > 2) |>
+    pull(family)
+  
+  # filter to subset of families
+  data_subset <- data_subset |>
+    filter(family %in% focal_families)
+  
+  # aggregate to species level
+  data_species <- data_subset |>
+    group_by(family, species_common, thermal_guild, popular_y) |>
+    summarize(across(starts_with('MCDM'), 
+                     ~ first(.x)),
+              across(starts_with('weighted_MCDM'), 
+                     ~ mean(.x, na.rm = TRUE)))
+  
+  # sort aggregated data
+  subset_species <- pull(data_species, species_common) |> sort(decreasing = TRUE)
+  data_species <- mutate(data_species, species_common = 
+                           factor(species_common, levels = subset_species))
+  
+  # aggregate to family level
+  data_family <- data_subset |>
+    group_by(family, thermal_guild, popular_y) |>
+    summarize(across(starts_with('MCDM'), 
+                     ~ first(.x)),
+              across(starts_with('weighted_MCDM'), 
+                     ~ mean(.x, na.rm = TRUE)))
+  
+  data_species |>
+    select(family, thermal_guild, species = species_common, 
+           cvi_2030 = weighted_MCDM_VUL_2030_45, 
+           cvi_2075 = weighted_MCDM_VUL_2075_45) |>
+    left_join(select(data_family, family, thermal_guild, 
+                     cvi_2030_family = weighted_MCDM_VUL_2030_45,
+                     cvi_2075_family = weighted_MCDM_VUL_2075_45))|>
+    readr::write_csv(out_file)
+  
   return(out_file)
 }
\ No newline at end of file
diff --git a/public/fish_as_food_climate.csv b/public/fish_as_food_climate.csv
new file mode 100644
index 0000000000000000000000000000000000000000..bb9e48fb6f2ab706d71eb2643c130a7db25dfca7
--- /dev/null
+++ b/public/fish_as_food_climate.csv
@@ -0,0 +1,142 @@
+family,thermal_guild,species,cvi_2030,cvi_2075,cvi_2030_family,cvi_2075_family
+Alestidae,warm,Big-scale tetra,0.012274708350000002,0.0108627154,0.11286009582760641,0.10272450171172955
+Alestidae,warm,Nurse tetra,0.012274708350000002,0.0108627154,0.11286009582760641,0.10272450171172955
+Alestidae,warm,Tigerfish,0.17991702081267735,0.1639656925862159,0.11286009582760641,0.10272450171172955
+Anguillidae,warm,Australian eel,0.0019279156693999997,0.0018898418147999998,0.060088577503724185,0.05356143600511973
+Anguillidae,warm,European eel,0.07434074572260523,0.06609150511026632,0.060088577503724185,0.05356143600511973
+Anguillidae,warm,Japanese eel,0.0036558991076,0.0042168184693999996,0.060088577503724185,0.05356143600511973
+Centrarchidae,warm,Bass,0.0657804855974,0.062753579817,0.06571650285962258,0.06199132366577771
+Centrarchidae,warm,Black bass,0.06640203166666667,0.0643960009,0.06571650285962258,0.06199132366577771
+Centrarchidae,warm,Crappie,0.0699718436,0.06477207332000001,0.06571650285962258,0.06199132366577771
+Centrarchidae,warm,Largemouth bass,0.07146540516942362,0.06686251627901083,0.06571650285962258,0.06199132366577771
+Centrarchidae,warm,Sunfish,0.0673828433562,0.062317355349150005,0.06571650285962258,0.06199132366577771
+Centrarchidae,warm,"White bass, striped bass",0.0386972223,0.0352063888,0.06571650285962258,0.06199132366577771
+Characidae,warm,Dorado,0.012366997772518243,0.011679929906890194,0.03036102208400994,0.028133410406297642
+Characidae,warm,Goldenfish,0.09620422520000001,0.08895133840000001,0.03036102208400994,0.028133410406297642
+Characidae,warm,Macabil,0.0801381195916,0.0740964648872,0.03036102208400994,0.028133410406297642
+Characidae,warm,Sabaleta,0.012333875025147671,0.011404017743133585,0.03036102208400994,0.028133410406297642
+Characidae,warm,Sabalo del Patía,0.012333875025147671,0.011404017743133585,0.03036102208400994,0.028133410406297642
+Characidae,warm,South American trout,0.004810211260000001,0.00444756692,0.03036102208400994,0.028133410406297642
+Characidae,warm,"Yamú, Bocón, Sábalo",0.012333875025147671,0.011404017743133585,0.03036102208400994,0.028133410406297642
+Cichlidae,warm,Bay snook (Tenguayaca),0.052899941706100004,0.0543490282988,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,"Mojarra Plateada, Tilapia",0.01490390997376282,0.01381896607637032,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Mozambique tilapia,0.00698411364,0.00638055316,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Oscar,0.008136825204802732,0.008359717025306638,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Pavón,0.008136825204802732,0.008359717025306638,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Pavón Cinchado - Pinta de Lapa,0.007861983512506034,0.008050564307370287,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Pavón Mariposo,0.008136825204802732,0.008359717025306638,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Peacock Bass,0.0317336183,0.0326028964,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Speckled pavon,0.030661735699999998,0.031397200800000004,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Tilapia,0.0344154895108,0.031549818668399995,0.020603395776181064,0.020246516151158973
+Cichlidae,warm,Tilapia Negra,0.008953991845795688,0.00818019635864715,0.020603395776181064,0.020246516151158973
+Clariidae,warm,African catfish,0.012784239967400001,0.0122263953392,0.027590318372466668,0.02638640551306667
+Clariidae,warm,African sharptooth catfish,0.0466578101,0.044621880800000006,0.027590318372466668,0.02638640551306667
+Clariidae,warm,North African catfish,0.02332890505,0.022310940400000003,0.027590318372466668,0.02638640551306667
+Cyprinidae,warm,Albanian roach,0.10778174260000001,0.0941208422,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Asp,0.013429605127960001,0.011727456938119999,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Barbel,0.04705801130333334,0.039834021549,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Bata,0.0055121400746,0.007563008347000001,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,BeyÅŸehir bleak,7.267071073e-4,6.011377922000001e-4,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Black bream,0.051246241799999995,0.061345053,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Black carp,0.051246241799999995,0.061345053,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Blue-finned mahseer,0.0224598681704,0.027820540914799997,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Catla,0.0220743179623,0.0302873745485,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Chocolate mahseer,0.0028306322192,0.0034302133044,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Chub,0.03216167369029999,0.026581131487899996,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Common barbel,0.002834382148,0.0023992692516,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Common bleak,0.00854949538,0.007072209320000001,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Common bream,0.04600620713676522,0.03642865846998261,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Common carp,0.13811330662322036,0.1199294950838376,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Common nase,0.026387573782000004,0.023642661213400005,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Common roach,0.05312624687117,0.042622199202855,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Crucian carp,0.116377215108,0.10105514804045,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Cyprinds,0.032788926016,0.028471980271733335,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,European carp,0.13040639766,0.11323726735275001,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,European chub,0.001917238372,0.001584568196,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Ghonia,0.0018287941369,0.0025092223955,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Golden mahseer,0.0034355641156000004,0.0039133067584,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Goldfish,0.189583548,0.16462323395,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Grass carp,0.028623161305374997,0.034263768144375,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Gudgeon,0.0344069672688,0.030376054976399995,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Italian bleak,0.10233745969859999,0.0846543455604,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Kalibaus,0.0036833459377,0.0050537859515,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Kutum,2.15702238e-4,1.9326426059999998e-4,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Labeo,0.017643999771499996,0.024208694942499998,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Mrigal,0.019859158866899998,0.0272480347455,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Mullet,0.022048241277999998,0.018222534254,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Orange River mudfish,0.01803036473,0.024738812350000004,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Pale chub,0.0023231629616,0.0027809757360000003,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Pearl mullet,0.1777012614733,0.14699587071620002,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Prussian carp,0.064146947634,0.055701447087225,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Prussian/Crucian carp,0.0216666912,0.018814083880000002,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Razor fish (ziege),3.7723609910000005e-4,3.294229477e-4,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Red garra,0.017643999771499996,0.024208694942499998,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Rohu,0.0017515211452,0.0024031989140000003,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Rudd,0.010652671243479999,0.00881197281272,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Rui,0.0368077017131,0.0505025183545,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Shabout,0.0011991616780000001,0.0010150754526,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Silver bream,0.010815032339999999,0.008563564440000001,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Silver carp,0.029425103982114283,0.035223745194000004,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Tench,0.017184651140000003,0.015171401295,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Transcau casian barb,0.014553462183,0.0123193248111,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Trout barb,0.004578617316,0.0038757426371999995,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,Vimba bream,0.0212868941635,0.0185888663345,0.05987770694593892,0.052805491729310206
+Cyprinidae,warm,White Bream,0.043260129359999996,0.034254257760000004,0.05987770694593892,0.052805491729310206
+Percidae,cool,Eurasian Ruffe,0.00486753292,0.0043199098100000005,0.053766659159614545,0.04716731515478815
+Percidae,cool,European perch,0.07259463927767063,0.06347059881833862,0.053766659159614545,0.04716731515478815
+Percidae,cool,Golden perch,0.0334276893426,0.0344360226346,0.053766659159614545,0.04716731515478815
+Percidae,cool,Redfin perch,0.0263136745872,0.0230064464784,0.053766659159614545,0.04716731515478815
+Percidae,cool,Volga pikeperch,8.01681972e-4,6.487112373e-4,0.053766659159614545,0.04716731515478815
+Percidae,cool,Walleye,0.10268783482319999,0.0983561899176,0.053766659159614545,0.04716731515478815
+Percidae,cool,Yellow perch,0.0488076651,0.0456058989,0.053766659159614545,0.04716731515478815
+Percidae,cool,Zander (pikeperch),0.037962706268244786,0.032533836325300367,0.053766659159614545,0.04716731515478815
+Pimelodidae,warm,"Amarillo, Toruno, Chontaduro",0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,"Bagre, Bagre Rayado",0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Barbiancho,0.008066403358651704,0.00795324982019469,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Barred sorubim,0.02347918176,0.02402206784,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Blancopobre,0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Blanquillo,0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Cajaro,0.008066403358651704,0.00795324982019469,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Cucharo,0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Flatwhiskered catfish,0.00629179462,0.006203534859999999,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Gilded catfish,0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Kumakuma (catfish),0.0293489772,0.030027584800000003,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Pati,0.00629179462,0.006203534859999999,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,"Pintadillo, Bagre, Bagre Rayado",0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Redtail catfish,0.00629179462,0.006203534859999999,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,South American catfish,0.0293489772,0.030027584800000003,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Spotted sorubim,0.0293489772,0.030027584800000003,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,"Tigrito, tijero",0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,"Valentón, Plumita",0.007525378768929754,0.007699380717640743,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,White catfish,0.00629179462,0.006203534859999999,0.010981231987585237,0.011153188357481665
+Pimelodidae,warm,Yellow catfish,0.009437691929999999,0.00930530229,0.010981231987585237,0.011153188357481665
+Salmonidae,cold,Arctic Char,0.0356557719054,0.03500071147903334,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Atlantic salmon,0.265177841604771,0.24011746662753772,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Balkan trout,0.026710147320000002,0.023695444160000002,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Brook Trout,0.026461932224000003,0.024708687848000004,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Brown Trout,0.050732109690129576,0.04500611164287132,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Cherry trout,0.0029371095160000004,0.0027625745076,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Chinook Salmon (Quinnat Salmon),0.1600752957,0.15222991949999998,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Chum salmon,0.02885045315,0.0284171047,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Coho salmon,0.2150947458,0.20066481688,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Common huchen,5.120283490000001e-5,4.81393951e-5,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Dolly varden/Char,0.0034788601644,0.0033581483528,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,European grayling,0.024065881522700002,0.0212009485031,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Pink salmon,0.025550719900000005,0.02443988385,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Rainbow trout,0.06302419030074105,0.05967045472608766,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Salmon,0.06449020412644443,0.06255334611855555,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Sockeye salmon,0.029446763100000002,0.029350874649999997,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Steelhead,0.0049289009400000005,0.00466661704,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Trout,0.0858357697255,0.08267910955158334,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Vendace,0.04116622401,0.0383635935,0.06944668713248901,0.06438766096040704
+Salmonidae,cold,Whitefish,0.05007684609903145,0.04666757308221,0.06944668713248901,0.06438766096040704
+Serrasalmidae,warm,"Cachama, Morocoto",0.008435958589406153,0.008389041102228541,0.023958122394703078,0.02382487673111427
+Serrasalmidae,warm,"Cachama, Pacú",0.008435958589406153,0.008389041102228541,0.023958122394703078,0.02382487673111427
+Serrasalmidae,warm,"Cachara, Tambaqui",0.055930405450000005,0.05561934251,0.023958122394703078,0.02382487673111427
+Serrasalmidae,warm,"Cherna, Cachama Negra",0.008435958589406153,0.008389041102228541,0.023958122394703078,0.02382487673111427
+Serrasalmidae,warm,Pacu,0.031255226575,0.031081397285000006,0.023958122394703078,0.02382487673111427
+Siluridae,warm,Boal,0.0124330956288,0.0105118070912,0.0286296405472,0.024205496966133333
+Siluridae,warm,Catfish,0.033215868223999995,0.028083014042666665,0.0286296405472,0.024205496966133333
+Siluridae,warm,European catfish,0.016684117296,0.014105917603999998,0.0286296405472,0.024205496966133333
+Siluridae,warm,Far eastern catfish,0.0031082739072,0.0026279517728,0.0286296405472,0.024205496966133333
+Siluridae,warm,Wels catfish,0.0333433019136,0.028190755380945457,0.0286296405472,0.024205496966133333
diff --git a/public/fish_as_food_climate_test.csv b/public/fish_as_food_climate_test.csv
new file mode 100644
index 0000000000000000000000000000000000000000..d6846eb1a5f634c2537ad075ddcabf40ebdd9f77
--- /dev/null
+++ b/public/fish_as_food_climate_test.csv
@@ -0,0 +1,13 @@
+family,thermal_guild,species,cvi_2030,cvi_2075,cvi_2030_family,cvi_2075_family
+A,warm,species_1,0.319953022,0.35962678,0.85151318,0.020097514
+A,warm,species_2,0.224863212,0.194521316,0.85151318,0.020097514
+A,warm,species_3,0.835617044,0.810848349,0.85151318,0.020097514
+A,warm,species_4,0.68844979,0.080709824,0.85151318,0.020097514
+A,warm,species_5,0.506342706,0.733569142,0.85151318,0.020097514
+A,warm,species_6,0.313221865,0.058278019,0.85151318,0.020097514
+B,cool,species_7,0.484686402,0.646526582,0.053519735,0.775921332
+B,cool,species_8,0.943640492,0.319394434,0.053519735,0.775921332
+B,cool,species_9,0.894768952,0.914064253,0.053519735,0.775921332
+B,cool,species_10,0.069633857,0.332752341,0.053519735,0.775921332
+C,cold,species_11,0.953508638,0.178160176,0.700810585,0.0658746
+C,cold,species_12,0.092467137,0.585798787,0.700810585,0.0658746
diff --git a/src/assets/content/ChartGrid.js b/src/assets/content/ChartGrid.js
index 6fb09ed3a3f1bc834222ff4909ed50a23961d881..235a54aca733bec64b052f7e1ee775369b22c380 100644
--- a/src/assets/content/ChartGrid.js
+++ b/src/assets/content/ChartGrid.js
@@ -11,7 +11,17 @@ export default {
             alt: '',
             chartOrder: 1,
             description: 'Inland fisheries are threatened.'
-        },          
+        },     
+        {
+            title: 'Climate vulnerability of recreationally fished inland fish species',
+            project: 'Fish as Food',
+            vizKey: 'FishAsFoodLinkChart',
+            vizRoute: 'inland-rec-fish-climate',
+            img_src: 'FishAsFoodLinkChart_thumbnail.png',
+            chartOrder: 1,
+            alt: '',
+            description: 'Inland fish that are recreationally fished are vulnerable'
+        },
         {
             title: 'Glacier/Topography D3 Cross-Section Scan',
             project: 'Fire in Ice',
@@ -29,7 +39,7 @@ export default {
             vizRoute: 'inland-rec-fish-value',
             img_src: 'circle-pack-thumbnail.png',
             alt: '',
-            chartOrder: 1,
+            chartOrder: 2,
             description: 'Inland recreational fishing contributes economic value.'
         },
         {
diff --git a/src/assets/svgs/climate-vulnerability-legend.svg b/src/assets/svgs/climate-vulnerability-legend.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2f93da6ff3f5195078ab85249dfe6090b5cec78d
--- /dev/null
+++ b/src/assets/svgs/climate-vulnerability-legend.svg
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="chart-svg" xmlns="http://www.w3.org/2000/svg" width="350" height="100" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 350 100">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: url(#linear-gradient-2);
+      }
+
+      .cls-2 {
+        fill: #231f20;
+        font-family: SourceSansPro-Bold, 'Source Sans Pro';
+        font-size: 12px;
+        font-weight: 700;
+      }
+
+      .cls-3, .cls-4 {
+        stroke-width: .5px;
+      }
+
+      .cls-3, .cls-4, .cls-5, .cls-6 {
+        fill: #fff;
+        stroke-miterlimit: 10;
+      }
+
+      .cls-3, .cls-6 {
+        stroke: #f37359;
+      }
+
+      .cls-4, .cls-5 {
+        stroke: #66a9dd;
+      }
+
+      .cls-5, .cls-6 {
+        stroke-width: 4px;
+      }
+
+      .cls-7 {
+        fill: url(#linear-gradient);
+      }
+    </style>
+    <linearGradient id="linear-gradient" x1="152.4" y1="217.7" x2="162.2" y2="217.7" gradientTransform="translate(-4043 499) scale(26.7 -1.9)" gradientUnits="userSpaceOnUse">
+      <stop offset=".4" stop-color="#66a9dd"/>
+      <stop offset="1" stop-color="#66a9dd" stop-opacity="0"/>
+    </linearGradient>
+    <linearGradient id="linear-gradient-2" x1="172.4" y1="237.6" x2="162.6" y2="237.6" gradientTransform="translate(-183.8 496) scale(2.9 -1.9)" gradientUnits="userSpaceOnUse">
+      <stop offset=".4" stop-color="#f37359"/>
+      <stop offset="1" stop-color="#f37359" stop-opacity="0"/>
+    </linearGradient>
+  </defs>
+  <g id="legend-visuals">
+    <g id="area-group-Percidae">
+      <path id="area-Percidae" class="cls-7" d="M292.2,78.6L31.1,86.9v-18.9l261.1,8.3v2.4Z"/>
+    </g>
+    <g id="area-group-Pimelodidae">
+      <path id="area-Pimelodidae" class="cls-1" d="M292.2,37.1l28.6,8.3v-18.9l-28.6,8.3v2.4Z"/>
+    </g>
+    <g id="point-2030-group-Percidae">
+      <circle id="point-2030-Percidae_x5F_Zander_x5F__x28_pikeperch_x29_" class="cls-4" cx="292.2" cy="77.4" r="1.2"/>
+    </g>
+    <g id="point-2030-group-Pimelodidae">
+      <circle id="point-2030-Pimelodidae" class="cls-3" cx="292.2" cy="35.9" r="1.2"/>
+    </g>
+    <g id="point-2030-group-Percidae-2" data-name="point-2030-group-Percidae">
+      <circle id="point-2075-Percidae_x5F_Zander_x5F__x28_pikeperch_x29_" class="cls-5" cx="31.1" cy="77.4" r="7.4"/>
+    </g>
+    <g id="point-2030-group-Pimelodidae-2" data-name="point-2030-group-Pimelodidae">
+      <circle id="point-2075-Pimelodidae" class="cls-6" cx="320.7" cy="35.9" r="7.4"/>
+    </g>
+  </g>
+  <g id="legend-labels">
+    <text class="cls-2" transform="translate(279.4 60.5)"><tspan x="0" y="0">2030</tspan></text>
+    <text class="cls-2" transform="translate(18.3 60.5)"><tspan x="0" y="0">2075</tspan></text>
+    <text class="cls-2" transform="translate(308 19.3)"><tspan x="0" y="0">2075</tspan></text>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/src/assets/text/authors.js b/src/assets/text/authors.js
index 72306e803e0dc0f658b3fd0d594bff7828434756..253beb0f7278c267010abc37a3a02d311daf4664 100644
--- a/src/assets/text/authors.js
+++ b/src/assets/text/authors.js
@@ -167,4 +167,20 @@ export default {
     additionalAuthors: [
     ]
   },
+  FishAsFoodLinkChart: {
+    primaryAuthors: [
+      {
+        firstName: 'Hayley',
+        lastName: 'Corson-Dosch',
+        fullName: 'Hayley Corson-Dosch',
+        initials: 'HCD',
+        profile_link: 'https://www.usgs.gov/staff-profiles/hayley-corson-dosch',
+        
+        role: 'lead developer',
+        contribution: 'built this data visualization'
+      },
+    ],
+    additionalAuthors: [
+    ]
+  },
 };
\ No newline at end of file
diff --git a/src/assets/text/references.js b/src/assets/text/references.js
index 1280dd49ae39122b557c85d3f47121d9e8733da4..67a1a97f3dbd4ec5f6b7ae1e15fd8031a916eda5 100644
--- a/src/assets/text/references.js
+++ b/src/assets/text/references.js
@@ -191,6 +191,32 @@ export default {
       journal_issue: "23, 6",
       data_release: false,
       doi: "https://doi.org/10.1111/faf.12688"
+    },
+    {
+      reference: "Reference 6.",
+      num: "6",
+      authors: "Nyboer, E. A., Embke, H. S., Robertson, A. M., Arlinghaus, R., Bower, S., Baigun, C., Beard, D., Cooke, S. J., Cowx, I. G., Koehn, J. D., Lyach, R., Milardi, M., Potts, W., & Lynch, A. J.",
+      year: "2021",
+      title: "Data for: Nyboer et al. 2021 Global assessment of marine and freshwater recreational fish reveals mismatch in climate change vulnerability and conservation effort",
+      link: "https://osf.io/keajr/",
+      journal: false,
+      journal_name: "",
+      journal_issue: "",
+      data_release: false,
+      doi: "https://doi.org/10.17605/osf.io/keajr"
+    },
+    {
+      reference: "Reference 7.",
+      num: "7",
+      authors: "Nyboer, E.A.,  Lin, H., Bennett, J.R., Gabriel, J, Twardek, W., Chhor, A.D., Daly, L., Dolson, S., Guitard, E., Holder, P., Mozzon, C.M., Trahan, A., Zimmermann, D., Kesner-Reyes, K., Garilao, C., Kaschner, K., & Cooke, S.J.",
+      year: "2021",
+      title: "Global assessment of marine and freshwater recreational fish reveals mismatch in climate change vulnerability and conservation effort",
+      link: "https://onlinelibrary.wiley.com/doi/10.1111/gcb.15768?af=R",
+      journal: true,
+      journal_name: "Global Change Biology",
+      journal_issue: "527, 19",
+      data_release: false,
+      doi: "https://doi.org/10.1111/gcb.15768"
     }
   ],
   findex: [
diff --git a/src/assets/text/text.js b/src/assets/text/text.js
index b46b4b3376a1a9ba153dfdd6404c89594da618c2..1988c6e6db9e964f0aad6a280ac11e862a807916 100644
--- a/src/assets/text/text.js
+++ b/src/assets/text/text.js
@@ -99,6 +99,12 @@ export default {
         FishAsFoodCirclePacking: {
             paragraph1: "Explore the global economic value of recreationally-fished inland fish species, in U.S. dollars. Click on the circles in the diagram to see the economic value of species within each fish family, and click on the nested circles to see the economic value of each species in the various countries where it is recreationally fished.",
             paragraph2: "The total economic value for each species in each country is calculated by multiplying the total kilograms of bimoass harvested for each species by the price per kilogram, in U.S. dollars. Species- and country-specific price data were collected from November 2021 to February 2022."
+        },
+        FishAsFoodLinkChart: {
+            paragraph1: 'Climate vulnerability index. Climate vulnerability varies by family and species. <span class="warm-text"><b>warm</b></span>, <span class="cool-text"><b>cool</b></span>, or <span class="cold-text"><b>cold</b></span> thermal guilds.',
+            explainerPart1: 'Showing the ',
+            explainerPart2: 'from 2030 to 2075 in harvest-weighted climate vulnerability under representative concentration pathway 4.5.',
+            prompt1: 'Click on the chart to show or hide data for <i>species</i> within each <b>family</b>'
         }
     }
 }
\ No newline at end of file
diff --git a/src/components/FishAsFoodLinkChartViz.vue b/src/components/FishAsFoodLinkChartViz.vue
new file mode 100644
index 0000000000000000000000000000000000000000..11c030608f8a8bd48a7e392d255acd146ca10d2c
--- /dev/null
+++ b/src/components/FishAsFoodLinkChartViz.vue
@@ -0,0 +1,766 @@
+<template>
+    <!---VizSection-->
+    <VizSection
+        :figures="true"
+        :fig-caption="false"
+    >
+        <!-- HEADING -->
+        <template #heading>
+        </template>
+        <!-- FIGURES -->
+        <template #aboveExplanation>
+            <p v-html="text.paragraph1" />
+            <p>
+                {{ text.explainerPart1 }}
+                <span>
+                    <button
+                        aria-pressed="!scalePercent"
+                        class="button"
+                        :text="scaleType"
+                        @click="toggleScale"
+                        >
+                        {{ scaleType }}
+                    </button>
+                </span>
+                {{ text.explainerPart2 }}
+            </p>
+            <p v-html="text.prompt1" />
+            <legendSVG />
+        </template>
+        <template #figures>
+            <div id="chart-container" class="maxWidth" ref="chart"></div>
+        </template>
+        <!-- FIGURE CAPTION -->
+        <template #figureCaption>
+        </template>
+        <!-- EXPLANATION -->
+        <template #belowExplanation>
+        </template>
+    </VizSection>
+</template>
+
+<script setup>
+    import { computed, onMounted, reactive, ref } from "vue"; //, reactive
+    import * as d3 from 'd3';
+    import VizSection from '@/components/VizSection.vue';
+    import legendSVG from '@/assets/svgs/climate-vulnerability-legend.svg'
+
+    // define props
+    defineProps({
+        text: { type: Object }
+    })
+
+    // global variables
+    const publicPath = import.meta.env.BASE_URL;
+    const initialLoad = ref();
+    const dataFile = 'fish_as_food_climate.csv' //'fish_as_food_climate_test.csv'
+    const data = ref();
+    const families = ref();
+    const chart = ref(null);
+    const scalePercent = ref(false);
+    let chartDimensions;
+    const chartTitle = 'Title of chart';
+    let chartSVG;
+    let chartBounds;
+    let xScale;
+    let xAxisBottom;
+    let yScale;
+    const rowHeight = 25;
+    let yAxis;
+    let widthScale;
+    const colors = {warm: '#FF7256', cool: '#5CACEE', cold: '#36648B'}
+    let colorScale;
+    const transitionLength = 1500;
+    // Create a reactive object to track expanded families
+    const expandedFamilies = reactive({});
+
+    // set up filtered chart data as computed property
+    const scaleType = computed(() => {
+        return scalePercent.value ? 'percent change' : 'change'
+    });
+
+    // Behavior on mounted (functions called here)
+    // Load data and then make chart
+    onMounted(async () => {
+        try {
+            await loadDatasets();
+            initialLoad.value = true;
+
+            if (data.value.length > 0) {
+                families.value = Array.from(new Set(data.value.map(d => d.family)));
+                // Initialize the expandedFamilies object
+                families.value.forEach(family => {
+                    expandedFamilies[family] = false;
+                });
+
+                initChart({
+                    width: chart.value.offsetWidth,
+                    margin: 25,
+                    marginBottom: 95,
+                    marginLeft: 350});
+
+                drawChart(data.value, scalePercent.value);
+                initialLoad.value = false;
+            } else {
+                console.error('Error loading data');
+            }
+        } catch (error) {
+            console.error('Error during component mounting', error);
+        }
+    });
+
+    async function loadDatasets() {
+        try {
+            data.value = await loadData(dataFile);
+            console.log('data in');
+        } catch (error) {
+            console.error('Error loading datasets', error);
+        }
+    }
+
+    async function loadData(fileName) {
+        try {
+            const data = await d3.csv(publicPath + fileName, d => {
+                d.cvi_2030 = +d.cvi_2030;
+                d.cvi_2075 = +d.cvi_2075;
+                d.cvi_2030_family = +d.cvi_2030_family;
+                d.cvi_2075_family = +d.cvi_2075_family;
+                return d;
+            });
+            return data;
+        } catch (error) {
+            console.error(`Error loading data from ${fileName}`, error);
+            return [];
+        }
+    }
+
+    function toggleScale() {
+        scalePercent.value = !scalePercent.value
+        drawChart(data.value, scalePercent.value)
+    }
+
+    function initChart({
+        width = 500, // outer width, in pixels
+        margin = 1, // default margins
+        marginTop = margin, // top margin, in pixels
+        marginBottom = margin, // left margin, in pixels
+        marginLeft = margin, // left margin, in pixels
+        marginRight = margin // right margin, in pixels
+    }) {
+        // set up global chart dimensions
+        chartDimensions = {
+            width,
+            margin: {
+                top: marginTop,
+                right: marginRight,
+                bottom: marginBottom,
+                left: marginLeft
+            }
+        }
+        chartDimensions.boundedWidth = chartDimensions.width - chartDimensions.margin.left - chartDimensions.margin.right
+
+        // draw canvas for chart
+        chartSVG = d3.select("#chart-container")
+            .append("svg")
+                // viewbox set on draw, since height is dynamic
+                .attr("width", "100%")
+                .attr("height", "100%")
+                .attr("id", "chart-svg")
+
+        // assign role for accessibility
+        chartSVG.attr("role", "figure")
+            .append("title")
+            .text(chartTitle)
+
+        // Add group for bounds
+        chartBounds = chartSVG.append("g")
+            .attr("id", "chart-bounds")
+            .style("transform", `translate(${
+                chartDimensions.margin.left
+            }px, ${
+                chartDimensions.margin.top
+            }px)`)
+
+        // Initialize axes
+        initXAxis()
+        initYAxis()
+
+        // Initialize scales
+        initXScale()
+        initYScale()
+
+        // Inititalize gradients
+        initGradients()
+
+        // Add groups for visual elements
+        chartBounds.append("g")
+            .attr("class", "areas")
+        chartBounds.append("g")
+            .attr("class", "points_2030")
+        chartBounds.append("g")
+            .attr("class", "points_2075")
+        chartBounds.append("g")
+            .attr("class", "rects")
+    }
+
+    function initXScale() {
+        // scale for the x axis (domain set in `drawChart()`)
+        xScale = d3.scaleLinear()
+            .range([0, chartDimensions.boundedWidth]);
+    }
+    function initXAxis() {
+        // add group for x axis
+        // will translate in drawChart, since height dynamic
+        xAxisBottom = chartBounds.append("g")
+            .attr("id", "x-axis")
+            .attr("class", "axis")
+            .attr("aria-hidden", true) // hide from screen reader
+
+        // Not generating here, will generate in drawChart()
+        // generate x axis
+        // xAxisBottom
+        //     .call(d3.axisBottom(xScale).tickSize(0).tickPadding(10))
+            // .select(".domain").remove() // remove axis line
+        
+        // Not adding x-axis title here
+        // // add placeholder for x axis title (title text set in drawChart())
+        // xAxisBottom
+        //     .append("text")
+        //     .attr("class", "x-axis axis-title")
+        //     .attr("x", -chartDimensions.boundedWidth / 2)
+        //     .style("text-anchor", "middle")
+        //     .attr("role", "presentation")
+        //     .attr("aria-hidden", true)
+    }
+
+    function initYScale() {
+        // scale for y axis (domain and range set in `drawChart()`)
+        yScale = d3.scaleBand()
+            // .range([chartDimensions.boundedHeight, 0])
+            .padding(0.1)
+    }
+    function initYAxis() {
+        // add group for y axis
+        yAxis = chartBounds.append("g")
+            .attr("id", "y-axis")
+            .attr("class", "axis")
+            .attr("aria-hidden", true)
+
+        // Not generating here, will generate in drawChart()
+        // generate y axis
+        // yAxis
+        //     .call(d3.axisLeft(yScale).tickSize(2))
+            // .select(".domain").remove() // remove axis line
+
+        // Not adding y-axis title
+        // // add placeholder for y axis title (title text set in drawChart())
+        // yAxis
+        //     .append("text")
+        //     .attr("class", "y-axis axis-title")
+        //     .attr("x", -chartDimensions.boundedHeight / 2)
+        //     .attr("transform", "rotate(-90)")
+        //     .style("text-anchor", "middle")
+        //     .attr("role", "presentation")
+        //     .attr("aria-hidden", true)
+    }
+
+    function initGradients() {
+        Object.keys(colors).forEach(color => {
+            let lg_decreasing = chartSVG.append("defs").append("linearGradient")
+                .attr("id", color + "_gradient_decreasing")
+                .attr("x1", "0%")
+                .attr("x2", "100%")
+                .attr("y1", "0%")
+                .attr("y2", "0%");
+            lg_decreasing.append("stop")
+                .attr("offset", "40%")
+                .style("stop-color", colors[color])
+                .style("stop-opacity", 1)
+
+            lg_decreasing.append("stop")
+                .attr("offset", "100%")
+                .style("stop-color", colors[color])
+                .style("stop-opacity", 0)
+
+            let lg_increasing = chartSVG.append("defs").append("linearGradient")
+                .attr("id", color + "_gradient_increasing")
+                .attr("x1", "100%")
+                .attr("x2", "0%")
+                .attr("y1", "0%")
+                .attr("y2", "0%");
+            lg_increasing.append("stop")
+                .attr("offset", "40%")
+                .style("stop-color", colors[color])
+                .style("stop-opacity", 1)
+
+            lg_increasing.append("stop")
+                .attr("offset", "100%")
+                .style("stop-color", colors[color])
+                .style("stop-opacity", 0)
+        })
+    }
+
+    function initWidthScale(low, high) {
+        widthScale = d3.scaleLinear()
+            .domain([0, 1])
+            .range([low, high]);
+    }
+
+    function initColorScale(data) {
+        colorScale = d3.scaleOrdinal()
+            .domain(data)
+            .range(data.map(item => colors[item]));
+    }
+
+    function drawChart(data, scalePercent) {
+        ///////////////////////////////////////////
+        /////    SET UP ACCESSOR FUNCTIONS    /////
+        ///////////////////////////////////////////
+        const yAccessor = d => expandedFamilies[d.family] ? d.species : d.family //d.species
+        const xAccessor = d => d.cvi
+        const x0Accessor = d => scalePercent ? 0 : d.cvi_2030
+        const x1Accessor = d => scalePercent ? (d.cvi_2075 - d.cvi_2030)/ d.cvi_2030 : d.cvi_2075
+        const x0Accessor_family = d => scalePercent ? 0 : d.cvi_2030_family
+        const x1Accessor_family = d => scalePercent ? (d.cvi_2075_family - d.cvi_2030_family)/ d.cvi_2030_family : d.cvi_2075_family
+        const widthAccessor = d => d.position
+        const colorAccessor = d => d.thermal_guild
+        const identifierAccessor = d => expandedFamilies[d.family] ? d.family + '_' + d.species.replace(/ /g,"_") : d.family;
+
+        ////////////////////////////////
+        /////    SET UP X AXIS    /////
+        ///////////////////////////////
+        // set domain for xScale
+        if (scalePercent) {
+            const maxVal = d3.max([Math.abs(d3.max(data, x1Accessor)), Math.abs(d3.min(data, x1Accessor))])
+            xScale
+                .domain([-maxVal, maxVal])
+                .nice()
+        } else {
+            const maxVal = d3.max([d3.min(data, x0Accessor), d3.max(data, x0Accessor), d3.min(data, x1Accessor), d3.max(data, x1Accessor)])
+            xScale
+                .domain([0, Math.round(maxVal * 10) / 10])
+                .nice()
+        }
+        
+        // set tick format for x-axis
+        const xTickFormat = scalePercent ? d3.format("+.0%") : d3.format(".2f");
+        
+        // generate x-axis
+        xAxisBottom.transition(getUpdateTransition())
+            .call(d3.axisBottom(xScale).tickSize(5).tickPadding(10).tickFormat(xTickFormat))
+            .on("start", function(){
+                xAxisBottom.select(".domain").remove()
+            })
+
+        // assign class to x-axis text for css styling
+        xAxisBottom
+            .selectAll("text")
+            .attr("class", "axis-text")
+
+        // Add titles to x-Axis
+        const xAxisBottomLabelYPosition = xAxisBottom.select("text").attr('y')
+        const xAxisBottomLabelDy = xAxisBottom.select("text").attr('dy')
+        const labelOffsetFactor = 5
+        xAxisBottom.append("text")
+            .attr("class", "x-axis axis-title")
+            .attr("x", chartDimensions.boundedWidth / 2)
+            .attr("y", xAxisBottomLabelYPosition * labelOffsetFactor / 1.5)
+            .attr("dy", xAxisBottomLabelDy)
+            .style("text-anchor", "middle")
+            .text(scalePercent ? 'Percent change in harvest-weighted climate vulnerability, 2030-2075' : 'Change in harvest-weighted climate vulnerability, 2030-2075')
+        xAxisBottom.append("text")
+            .attr("class", "x-axis axis-subtitle")
+            .attr("x", 0)
+            .attr("y", xAxisBottomLabelYPosition * labelOffsetFactor)
+            .attr("dy", xAxisBottomLabelDy)
+            .style("text-anchor", "start")
+            .text('Less vulnerable')
+        xAxisBottom.append("text")
+            .attr("class", "x-axis axis-subtitle")
+            .attr("x", chartDimensions.boundedWidth)
+            .attr("y", xAxisBottomLabelYPosition * labelOffsetFactor)
+            .attr("dy", xAxisBottomLabelDy)
+            .style("text-anchor", "end")
+            .text('More vulnerable')
+
+        //////////////////////////////////////////
+        /////    SET UP Y AXIS AND HEIGHT    /////
+        //////////////////////////////////////////
+        // set domain for yScale
+        let yDomain = []
+        families.value.map(family => expandedFamilies[family] ? yDomain.push(data.filter(d => d.family === family).map(d => d.species)) : yDomain.push(family))
+        yDomain = yDomain.flat()
+        
+        // compute chart height
+        const totalHeight = yDomain.length * rowHeight;
+        chartDimensions.boundedHeight = totalHeight
+        chartDimensions.height = chartDimensions.boundedHeight + chartDimensions.margin.top + chartDimensions.margin.bottom
+        
+        // set viewbox for SVG
+        chartSVG
+            .transition(getUpdateTransition())
+            .attr("viewBox", [0, 0, (chartDimensions.width), (chartDimensions.height)].join(' '))
+        
+        // set y position for xAxisBottom, transitioning it if NOT initial load
+        if (initialLoad.value) {
+            xAxisBottom
+                .attr("transform", `translate(0,${chartDimensions.boundedHeight})`)
+        } else {
+            xAxisBottom
+                .transition(getUpdateTransition())
+                .attr("transform", `translate(0,${chartDimensions.boundedHeight})`)
+        }
+
+        // Set range and domain for y scale
+        yScale
+            .range([0, chartDimensions.boundedHeight])
+            .domain(yDomain)
+                    
+        // generate y axis
+        const xBuffer = 10;
+        yAxis
+            .transition(getUpdateTransition())
+            .call(d3.axisLeft(yScale).tickSize(- chartDimensions.boundedWidth))
+            .on("start", function(){
+                yAxis.select(".domain").remove();
+                yAxis
+                    .selectAll("text")
+                    .attr("x", -xBuffer)
+            })
+            // repeat text position b/c of weird quirk
+            .selectAll("text")
+                .attr("x", -xBuffer)
+
+        // assign class to y-axis labels
+        yAxis
+            .selectAll("text")
+            .attr("class", d => {
+                const current_data = data.filter(item => item.species === d)[0]
+                const current_family = current_data ? current_data.family : d
+                return expandedFamilies[d] === false ? "axis-text family " + current_family : "axis-text species family" + current_family
+            })
+        
+        // Add family labels for expanded families, waiting until chart is rendered to determine x placement
+        // start by making sure all existing labels are removed
+        families.value.forEach(family => {
+            if (expandedFamilies[family]) {
+                setTimeout(() => {
+                    // confirm family is still expanded
+                    if (expandedFamilies[family]) {
+                        const firstLabel = yAxis.select('.family' + family);
+                        const firstLabelWidth = firstLabel.node().getComputedTextLength()
+                        const firstLabelData = data.filter(d => d.family === family)[0]
+                        const firstLabelYPosition = yScale(yAccessor(firstLabelData))
+                        const yAxisLabelDy = yAxis.select("text").attr('dy')
+                        const yAxisLabelXPosition = yAxis.select("text").attr('x')
+                        const yAxisLabelDx = yAxis.select("text").attr('dx')
+                        yAxis.append("text")
+                            .attr("class", "y-axis axis-title familyTitle" + family)
+                            .attr("y", firstLabelYPosition + yScale.bandwidth() / 2)
+                            .attr("x", yAxisLabelXPosition - firstLabelWidth - xBuffer)
+                            .attr("dy", yAxisLabelDy)
+                            .attr("dx", yAxisLabelDx)
+                            .style("text-anchor", "end")
+                            .text(family)
+                            .style("opacity", 0)
+                            .transition(getUpdateTransition())
+                            .style("opacity", 1)
+                    }
+                }, transitionLength * 1.1); // give the chart a little extra time to render
+            }
+        });
+
+        ////////////////////////////////////
+        /////    SET UP WIDTH SCALE    /////
+        ////////////////////////////////////
+        const radiusPosition0 = 1
+        const strokeRatio = 0.3
+        const strokeWidth1 = strokeRatio * yScale.bandwidth() / 2
+        const radiusPosition1 = (1 - strokeRatio) * yScale.bandwidth() / 2
+        initWidthScale(radiusPosition0, radiusPosition1)
+
+        /////////////////////////////////////
+        /////    SET UP AREA FUNCTION   /////
+        /////////////////////////////////////
+        const area = d3.area()
+            .x(d => xScale(xAccessor(d)))
+            .y0(d => yScale(yAccessor(d)) + yScale.bandwidth() / 2 - widthScale(widthAccessor(d)))
+            .y1(d => yScale(yAccessor(d)) + yScale.bandwidth() / 2+ widthScale(widthAccessor(d)));
+
+        ///////////////////////////////////
+        /////    SET UP COLOR SCALE   /////
+        ///////////////////////////////////
+        const colorCategories = Array.from(new Set(data.map(colorAccessor)))
+        initColorScale(colorCategories)
+
+        ///////////////////////////////////////
+        /////    SET UP DATA FOR AREAS    /////
+        ///////////////////////////////////////
+        const areaCategories = Array.from(new Set(data.map(d => d.species)))
+        const areaData = areaCategories.map(areaCategory => {
+            const species = areaCategory
+            const family = data.filter(d => d.species === species)[0].family
+            const cvi_2030 = expandedFamilies[family] ? x0Accessor(data.filter(d => d.species === species)[0]) : x0Accessor_family(data.filter(d => d.family === family)[0])
+            const cvi_2075 = expandedFamilies[family] ? x1Accessor(data.filter(d => d.species === species)[0]) : x1Accessor_family(data.filter(d => d.family === family)[0])
+            const cvi_decreasing = cvi_2030 > cvi_2075
+            const thermal_guild = colorAccessor(data.filter(d => d.species === species)[0])
+
+            return [
+                {
+                    species: species,
+                    family: family,
+                    cvi: cvi_2030,
+                    cvi_decreasing: cvi_decreasing,
+                    position: 0,
+                    thermal_guild: thermal_guild
+                },
+                {
+                    species: species,
+                    family: family,
+                    cvi: cvi_2075,
+                    cvi_decreasing: cvi_decreasing,
+                    position: 1,
+                    thermal_guild: thermal_guild
+                }
+            ]
+        })
+        
+        ////////////////////////////////////
+        /////    ADD CHART ELEMENTS    /////
+        ////////////////////////////////////
+        // draw chart
+        // Enter-update-exit pattern for areas
+        let areaGroups = chartBounds.selectAll(".areas")
+            .selectAll(".area")
+            .data(areaData, d => d[0].species + d[0].family)
+        
+        const oldAreaGroups = areaGroups.exit()
+
+        oldAreaGroups.selectAll('path')
+            .transition(getExitTransition())
+            .style("opacity", 0)
+
+        oldAreaGroups.transition(getExitTransition()).remove()
+        
+        const newAreaGroups = areaGroups.enter().append("g")
+            .attr("class", d => "area " + d[0].species)
+            .attr("id", d => 'area-group-' + identifierAccessor(d[0]))
+
+        // append paths
+        newAreaGroups.append("path")
+            .append('path')
+            .attr("id", d => 'area-' + identifierAccessor(d[0]))
+            .attr('d', null)
+            .attr('fill', d => d[0].cvi_decreasing ? `url(#${d[0].thermal_guild}_gradient_decreasing)`: `url(#${d[0].thermal_guild}_gradient_increasing)`)//d => colorScale(colorAccessor(d[0])))
+            .style("opacity", 1)
+
+        // update areaGroups to include new paths
+        areaGroups = newAreaGroups.merge(areaGroups)
+
+        const areaPaths = areaGroups.select("path")
+
+        // Update paths based on data values
+        areaPaths.transition(getUpdateTransition())
+            .attr("id", d => 'area-' + identifierAccessor(d[0]))
+            .attr('d', d => area(d))
+            .attr('fill', d => d[0].cvi_decreasing ? `url(#${d[0].thermal_guild}_gradient_decreasing)`: `url(#${d[0].thermal_guild}_gradient_increasing)`)//d => colorScale(colorAccessor(d[0])))
+            .style("opacity", 1)
+
+        // Enter-Update-Exit pattern for 2030 points
+        let pointGroups2030 = chartBounds.selectAll('.points_2030')
+            .selectAll(".point_2030")
+            .data(data, d => d.species)
+        
+        const oldPointGroups2030 = pointGroups2030.exit()
+
+        oldPointGroups2030.selectAll('circle')
+            .transition(getExitTransition())
+            .style("opacity", 0)
+
+        oldPointGroups2030.transition(getExitTransition()).remove()
+        
+        const newPointGroups2030 = pointGroups2030.enter().append("g")
+            .attr("class", d => "point_2030 " + d.species)
+            .attr("id", d => 'point-2030-group-' + identifierAccessor(d))
+
+        // append points
+        newPointGroups2030
+            .append("circle")
+                .attr("id", d => 'point-2030-' + identifierAccessor(d))
+                .attr("class", "point_2030")
+                .attr("cx", d => expandedFamilies[d.family] ? xScale(x0Accessor(d)) : xScale(x0Accessor_family(d)))
+                .attr("cy", d => yScale(yAccessor(d)) + yScale.bandwidth() / 2)
+                .attr("r", radiusPosition0)
+                .style("stroke", d => colorScale(colorAccessor(d)))
+                .style("fill", "white")
+
+        // update pointGroups2030 to include new points
+        pointGroups2030 = newPointGroups2030.merge(pointGroups2030)
+
+        const allPoints2030 = pointGroups2030.select("circle")
+
+        // Update points based on data values
+        allPoints2030.transition(getUpdateTransition())
+            .attr("id", d => 'point-2030-' + identifierAccessor(d))
+            .attr("class", "point_2030")
+            .attr("cx", d => expandedFamilies[d.family] ? xScale(x0Accessor(d)) : xScale(x0Accessor_family(d)))
+            .attr("cy", d => yScale(yAccessor(d)) + yScale.bandwidth() / 2)
+            .attr("r", radiusPosition0)
+            .style("stroke", d => colorScale(colorAccessor(d)))
+            .style("fill", "white")
+
+        // Enter-Update-Exit pattern for 2075 points
+        let pointGroups2075 = chartBounds.selectAll('.points_2075')
+            .selectAll(".point_2075")
+            .data(data, d => d.species)
+        
+        const oldPointGroups2075 = pointGroups2075.exit()
+
+        oldPointGroups2075.selectAll('circle')
+            .transition(getExitTransition())
+            .style("opacity", 0)
+
+        oldPointGroups2075.transition(getExitTransition()).remove()
+        
+        const newPointGroups2075 = pointGroups2075.enter().append("g")
+            .attr("class", d => "point_2075 " + d.species)
+            .attr("id", d => 'point-2030-group-' + identifierAccessor(d))
+
+        // append points
+        newPointGroups2075
+            .append("circle")
+                .attr("id", d => 'point-2075-' + identifierAccessor(d))
+                .attr("class", "point_2075")
+                .attr("cx", d => expandedFamilies[d.family] ? xScale(x1Accessor(d)) : xScale(x1Accessor_family(d)))
+                .attr("cy", d => yScale(yAccessor(d)) + yScale.bandwidth() / 2)
+                .attr("r", radiusPosition1 - strokeWidth1 / 2)
+                .style("stroke", d => colorScale(colorAccessor(d)))
+                .style("stroke-width", strokeWidth1)
+                .style("fill", "white")
+
+        // update pointGroups2075 to include new points
+        pointGroups2075 = newPointGroups2075.merge(pointGroups2075)
+
+        const allPoints2075 = pointGroups2075.select("circle")
+
+        // Update points based on data values
+        allPoints2075.transition(getUpdateTransition())
+            .attr("id", d => 'point-2075-' + identifierAccessor(d))
+            .attr("class", "point_2075")
+            .attr("cx", d => expandedFamilies[d.family] ? xScale(x1Accessor(d)) : xScale(x1Accessor_family(d)))
+            .attr("cy", d => yScale(yAccessor(d)) + yScale.bandwidth() / 2)
+            .attr("r", radiusPosition1 - strokeWidth1 / 2)
+            .style("stroke", d => colorScale(colorAccessor(d)))
+            .style("stroke-width", strokeWidth1)
+            .style("fill", "white")
+
+        // Enter-Update-Exit pattern for overlay rectangles
+        let rectGroups = chartBounds.selectAll('.rects')
+            .selectAll(".rect")
+            .data(data, d => d.species)
+        
+        const oldRectGroups = rectGroups.exit()
+
+        oldRectGroups.selectAll('rect')
+            .transition(getExitTransition())
+            .style("opacity", 0)
+
+        oldRectGroups.transition(getExitTransition()).remove()
+        
+        const newRectGroups = rectGroups.enter().append("g")
+            .attr("class", d => "rect " + d.species)
+            .attr("id", d => 'rect-group-' + identifierAccessor(d))
+
+        // append rectangles
+        newRectGroups
+            .append("rect")
+                .attr("id", d => 'rect-' + identifierAccessor(d))
+                .attr("class", "rect")
+                .attr("x", -chartDimensions.margin.left)
+                .attr("y", d => yScale(yAccessor(d)))
+                .attr("height", yScale.bandwidth())
+                .attr("width", chartDimensions.width)
+                .style("fill", "transparent")
+
+        // update rectGroups to include new rectangles
+        rectGroups = newRectGroups.merge(rectGroups)
+
+        const allRects = rectGroups.select("rect")
+
+        // Update rectangles based on data values
+        allRects.transition(getUpdateTransition())
+            .attr("id", d => 'rect-' + identifierAccessor(d))
+            .attr("class", "rect")
+            .attr("x", -chartDimensions.margin.left)
+            .attr("y", d => yScale(yAccessor(d)))
+            .attr("height", yScale.bandwidth())
+            .attr("width", chartDimensions.width)
+            .style("fill", "transparent")
+        
+        allRects
+            .on("click", function(event, d) {
+                const clickedFamily = d.family
+                expandedFamilies[clickedFamily] = !expandedFamilies[clickedFamily]
+
+                // redraw chart
+                drawChart(data, scalePercent)
+            });
+    }
+
+    function getUpdateTransition () {
+      return d3.transition()
+        .duration(transitionLength)
+        .ease(d3.easeCubicInOut)
+    }
+    function getExitTransition() {
+      return d3.transition()
+        .duration(transitionLength)
+        .ease(d3.easeCubicInOut)
+    }
+</script>
+
+<style>
+    .warm-text {
+        color: #DB2500;
+    }
+    .cool-text {
+        color: #1474C2;
+    }
+    .cold-text {
+        color: #36648B;
+    }
+</style>
+
+<style lang="scss">
+    .axis-text {
+        font-size: 1.8rem;
+        font-family: var(--default-font);
+        user-select: none;
+    }
+    .species {
+        font-style: italic;
+    }
+    .family {
+        font-weight: 900;
+    }
+    .axis-title {
+        font-size: 1.8rem;
+        font-family: var(--default-font);
+        font-weight: 900;
+        fill: var(--color-text);
+        user-select: none;
+    }
+    .axis-subtitle {
+        font-size: 1.8rem;
+        font-family: var(--default-font);
+        font-weight: 400;
+        font-style: italic;
+        fill: var(--color-text);
+        user-select: none;
+    }
+    .tick line {
+        stroke: #0f0f0f;
+        stroke-width: 0.5px;
+        stroke-dasharray: 1, 2;
+    }
+</style>
\ No newline at end of file
diff --git a/src/components/Viz3Viz.vue b/src/components/Viz3Viz.vue
deleted file mode 100644
index 5f149d4b6a7a2c15cfbbcf48b0605a0ad61fb6f9..0000000000000000000000000000000000000000
--- a/src/components/Viz3Viz.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<template>
-    <!---VizSection-->
-    <VizSection
-        :figures="true"
-        :fig-caption="false"
-    >
-        <!-- HEADING -->
-        <template #heading>
-            <h2>
-                This is the Viz 3 viz!
-            </h2>
-        </template>
-        <!-- FIGURES -->
-        <template #aboveExplanation>
-        </template>
-        <template #figures>
-        </template>
-        <!-- FIGURE CAPTION -->
-        <template #figureCaption>
-        </template>
-        <!-- EXPLANATION -->
-        <template #belowExplanation>
-        </template>
-    </VizSection>
-</template>
-
-<script setup>
-    import VizSection from '@/components/VizSection.vue';
-
-    // define props
-    defineProps({
-        text: { type: Object }
-    })
-</script>
-
-<style>
-</style>
\ No newline at end of file