diff --git a/public/fii_core4biomass.csv b/public/fii_core4biomass.csv index 460d5a961de2f35c21a5664dddbd833fd4268122..99aa66fd340ff76590304c9dd26cbf9ac85dd349 100644 --- a/public/fii_core4biomass.csv +++ b/public/fii_core4biomass.csv @@ -1,7 +1,4 @@ depth_cm,year,site,vegetation_type,burned -420,2016,4,grass,TRUE -600,2016,4,grass,TRUE -710,2016,4,grass,TRUE 0,2016,4,softwood,TRUE 10,2016,4,softwood,TRUE 20,2016,4,softwood,TRUE diff --git a/src/assets/content/ChartGrid.js b/src/assets/content/ChartGrid.js index 1a456c3a986893e944b656308a00f973879cbc56..1e647557df1066336a969573651b1ef64f5b0bf0 100644 --- a/src/assets/content/ChartGrid.js +++ b/src/assets/content/ChartGrid.js @@ -7,20 +7,20 @@ export default { project: 'Findex', vizKey: 'ThreatSankey', vizRoute: 'inland-fish-total-threats', - img_src: 'Placeholder_thumbnail.png', + img_src: 'Placeholder_thumbnail.PNG', alt: '', chartOrder: 1, description: 'Land use change is threatening inland fisheries.' }, { - title: 'Explore the Juneau Ice Field', + title: 'Explore the Juneau Icefield', project: 'Fire in Ice', vizKey: 'GlacierScan', vizRoute: 'glacier-scan', img_src: 'glacial_mri_thumbnail.png', alt: '', chartOrder: 1, - description: 'Glacial Ice cores can record changes in wildfire prevalence.' + description: 'Glacier ice records change.' }, { title: 'Wildfire Aerosols', @@ -30,7 +30,7 @@ export default { img_src: 'wildfire_aerosols_thumbnail.png', alt: '', chartOrder: 2, - description: 'Wildfires are depositing aerosols on glaciers.' + description: 'Wildfire aerosols are preserved in glaciers.' }, { title: 'Regional wildfire deposition', @@ -40,7 +40,7 @@ export default { img_src: 'aerosols_map_thumbnail.png', alt: '', chartOrder: 2, - description: 'Particles from regional wildfires are deposited on glaciers.' + description: 'Smoke plumes from regional wildfires affect glaciers.' }, { title: 'Global economic value of recreationally fished species', diff --git a/src/assets/css/base.css b/src/assets/css/base.css index 0613744dcf861bdf3722faa7581266aba05091d1..436360c9fb3a5d3636a90f648fb953b0ec18ae1c 100644 --- a/src/assets/css/base.css +++ b/src/assets/css/base.css @@ -98,5 +98,5 @@ h3 { } } p, text, li { - padding: 0 0 1rem 0; + padding: 0 0 1.5rem 0; } diff --git a/src/assets/css/main.css b/src/assets/css/main.css index bee38a76ff9d4cdbd3916b343a0a3a235254104f..06b54a96ac4cf751f604e9f0d72735a6d60c61bf 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -81,7 +81,7 @@ a { color: #fff; text-align: center; border-radius: 6px; - padding: 5px; + padding: 0.9rem 0.65rem; position: absolute; z-index: 1; margin-left: -170px; diff --git a/src/assets/text/text.js b/src/assets/text/text.js index 09b6add735ac7a563c0f6427b12d00ad3e844c1c..bb3c3c1f06aa341b92eced3a9d84d9044ac9d464 100644 --- a/src/assets/text/text.js +++ b/src/assets/text/text.js @@ -20,7 +20,9 @@ export default { // keys must match project routes (with '-' replaced with '') findex: { title: "Findex", - motivation: "Global freshwater biodiversity faces unprecedented loss from rapid global change. Found in less than 0.01% of available surface water, inland fishes comprise 51% of all fish species and inland fisheries provide food for billions and livelihoods for millions of people worldwide. Despite their importance, inland fishes are some of the most threatened taxa on the planet from intensifying pressures, such as hydrological alterations, riparian degradation, invasive species, and climate change. One-third of all inland fishes are threatened with extinction. However, standardized methods to monitor and assess fisheries proves elusive because inland fisheries are highly dispersed with limited market integration. Here, we present the first global metric to examine threats to inland fisheries by river basin using literature synthesis, expert elicitation, and computational modeling. The resulting standardized assessment serves as a potential risk indicator for freshwater ecosystem status and its capacity to support inland fisheries. We show that most threats to inland fisheries come from outside the fishery sector, predominately from land use change. Given that inland fisheries are severely threatened and highly important with limited resources, this index can help direct, efficiently use, and mobilize limited resources for watershed management, sustainable fisheries, and ultimately human well-being.", + motivation: { + paragraph1: "Global freshwater biodiversity faces unprecedented loss from rapid global change. Found in less than 0.01% of available surface water, inland fishes comprise 51% of all fish species and inland fisheries provide food for billions and livelihoods for millions of people worldwide. Despite their importance, inland fishes are some of the most threatened taxa on the planet from intensifying pressures, such as hydrological alterations, riparian degradation, invasive species, and climate change. One-third of all inland fishes are threatened with extinction. However, standardized methods to monitor and assess fisheries proves elusive because inland fisheries are highly dispersed with limited market integration. Here, we present the first global metric to examine threats to inland fisheries by river basin using literature synthesis, expert elicitation, and computational modeling. The resulting standardized assessment serves as a potential risk indicator for freshwater ecosystem status and its capacity to support inland fisheries. We show that most threats to inland fisheries come from outside the fishery sector, predominately from land use change. Given that inland fisheries are severely threatened and highly important with limited resources, this index can help direct, efficiently use, and mobilize limited resources for watershed management, sustainable fisheries, and ultimately human well-being." + }, teamText: null, teamData: [ { name: "Gretchen Stokes", link: "", image: "https://labs.waterdata.usgs.gov/visualizations/headshots/GretchenStokes.png" }, @@ -29,7 +31,13 @@ export default { }, fireinice: { title: "Fire in Ice", - motivation: "Glaciers and ice caps around the world are melting. Glaciers serve as water towers and store freshwater that is essential for drinking water and agriculture. Losing this freshwater to the ocean results in rising sea levels. This glacial retreat is due to increased temperatures, water collecting under glaciers and hastening their movement, alterations in atmospheric circulation, and the deposition of dark aerosols on ice. Dark aerosols absorb heat and can increase melt on glacier surfaces. Black carbon is produced by both industrial burning and from forest fires. Nagorski et al. (2019) determined that black carbon is increasing the radiative forcing of the Juneau Icefield and accelerating the melt. Specific sugars (levoglucosan, mannosan, and galactosan) are only produced by burning vegetation including smoldering grass fires which are often hard to analyze in the past. As coal contains cellulose, coal burning can also produce levoglucosan, mannosan, and galactosan. Here, we collected a series of ~10 m cores across the Juneau Icefield, AK, which encompass some of the most rapidly melting glaciers on the planet, as well as a major contributor to sea level rise. Our goal was to determine if and how wildfire aerosols are affecting the Juneau Icefield. All three sugars were present in quantifiable amounts, demonstrating that wildfires are unequivocally depositing aerosols onto the icefield. Ratios between the sugars can provide information on if hardwoods, softwoods or grasses burned in the fires. The presence of hardwood fires, coupled with satellite data of smoke transport, suggest that fires as far away as East Asia may affect the Juneau Icefield.", + motivation: { + paragraph1: "Glaciers and ice caps around the world are melting.", + paragraph2: "Glaciers serve as water towers that store freshwater that is essential for drinking water and agriculture. When glaciers melt, this freshwater is lost to the ocean, raising sea levels. Glacial retreat is occurring due to increased temperatures, water collecting under glaciers and hastening their movement, alterations in atmospheric circulation, and the deposition of dark aerosols on ice.", + paragraph3: "Dark aerosols absorb heat and can increase melting on glacier surfaces. Black carbon, or soot, is a type of dark aerosol that is produced by both industrial burning of fossil fuels and forest fires. Nagorski et al. (2019) determined that black carbon increases the amount of heat absorbed by the Juneau Icefield and accelerates the melting.", + paragraph4: "The Fire in Ice Project collected a series of ~10-meter snow cores across the Juneau Icefield, AK, which includes some of the most rapidly melting glaciers on the planet and is a major contributor to sea level rise. The goal was to determine if and how wildfire aerosols are affecting the Juneau Icefield by studying material preserved in the core layers.", + paragraph5: "Scientists analyzed the core for specific sugars (levoglucosan, mannosan, and galactosan) that are only produced by burning vegetation. These sugars are transported in smoke plumes alongside dark aerosols and are also deposited on the icefield. All three sugars were present in quantifiable amounts, demonstrating that wildfires are unequivocally depositing aerosols onto the icefield. Ratios between the sugars can provide information on the vegetation type—hardwoods or softwoods—that burned in the fires. The presence of hardwood fires, coupled with satellite data of smoke transport, suggests that fires as far away as East Asia may affect the Juneau Icefield." + }, teamText: null, teamData: [ { name: "Natalie Kehrwald", link: "https://www.usgs.gov/staff-profiles/natalie-m-kehrwald", image: "https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/styles/staff_profile/public/media/images/NatalieKehrwald_cropped.jpg?itok=oLP9Dl1H" }, @@ -40,7 +48,9 @@ export default { }, fishasfood: { title: "Fish as Food", - motivation: "Inland recreational fishing, defined as primarily leisure-driven fishing in freshwaters, is a popular past-time which can provide substantial contributions to human consumption which are often overlooked at global scales. Here, we established a baseline of national inland recreational consumption estimates with species specificity to identify the nutritional composition, total use value, and climate vulnerability of this recreational consumption.", + motivation: { + paragraph1: "Inland recreational fishing, defined as primarily leisure-driven fishing in freshwaters, is a popular past-time which can provide substantial contributions to human consumption which are often overlooked at global scales. Here, we established a baseline of national inland recreational consumption estimates with species specificity to identify the nutritional composition, total use value, and climate vulnerability of this recreational consumption." + }, teamText: null, teamData: [ { name: "Holly Embke", link: "https://www.usgs.gov/staff-profiles/holly-embke", image: "https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/styles/staff_profile/public/media/images/embke.jpg?h=f1072bca&itok=xyG6frc8" }, @@ -50,7 +60,11 @@ export default { }, beaufortsea: { title: "Beaufort Sea", - motivation: "The Arctic Ocean is undergoing dramatic sea ice reduction and warming conditions. These changes affect the sealife of the region, including bottom-dwelling organisms and the marine mammals, seabirds, and fish that rely on them for food. The researchers of this project use microfossils from sediment cores taken in the Beaufort Sea to rebuild the climate patterns, sea ice and circulation, and ecosystems from the past 2000 years. By studying bottom-dwelling microorganisms like microscopic arthropods (called Ostracodes) and single-celled protists (called Foraminifera), the Beaufort Sea researchers can compare the historical conditions of the Arctic and better understand the effects of recent climate change in the region. Read more about this USGS science <a href='https://www.usgs.gov/programs/ecosystems-land-change-science-program/science/land-sea-linkages-arctic' target='_blank'>here</a>.", + motivation: { + paragraph1: "The Arctic Ocean is undergoing dramatic sea ice reduction and warming conditions.", + paragraph2: "These changes affect the sealife of the region, including bottom-dwelling organisms and the marine mammals, seabirds, and fish that rely on them for food. The researchers of this project use microfossils from sediment cores taken in the Beaufort Sea to rebuild the climate patterns, sea ice and circulation, and ecosystems from the past 2000 years.", + paragraph3: "By studying bottom-dwelling microorganisms like microscopic arthropods (called Ostracodes) and single-celled protists (called Foraminifera), the Beaufort Sea researchers can compare the historical conditions of the Arctic and better understand the effects of recent climate change in the region. Read more about this USGS science <a href='https://www.usgs.gov/programs/ecosystems-land-change-science-program/science/land-sea-linkages-arctic' target='_blank'>here</a>." + }, teamText: "Laura Gemery is an ecologist, and Julia Seidenstein, Jason Addison and Thomas Cronin are geologists within the <a href='https://www.usgs.gov/programs/ecosystems-land-change-science-program' target='_blank'>Ecosystems Land Change Science program</a> of the USGS Ecosystems Mission area. Each specialize in analyzing proxies in sediment cores.", teamData: [ { name: "Laura Gemery", link: "https://www.usgs.gov/staff-profiles/laura-gemery", image: "https://labs.waterdata.usgs.gov/visualizations/headshots/l_Gemery mustang Oden.png" }, @@ -62,47 +76,47 @@ export default { }, visualizations: { GlacierScan: { - paragraph1: "The Juneau Icefield is just north of Juneau, Alaska and extends into Canada. In 2016 and 2017, researchers collected ice cores across the icefield to determine if the ice traps records of wildfires.", - paragraph1Mobile: "The Juneau Icefield is just north of Juneau, Alaska. In 2016 and 2017, researchers collected ice cores across the icefield to determine if the ice traps records of wildfires.", + paragraph1: "The Juneau Icefield is just north of Juneau, Alaska and extends into Canada. In 2016 and 2017, researchers collected snow cores across the icefield to determine if the ice traps records of wildfires.", + paragraph1Mobile: "The Juneau Icefield is just north of Juneau, Alaska. In 2016 and 2017, researchers collected snow cores across the icefield to determine if the ice traps records of wildfires.", promptDesktop: "Hover over the map to explore the topography and learn about coring a glacier.", - promptMobile: "Click on the map below to explore the topography and learn about coring a glacier.", + promptMobile: "Tap the map to explore the topography and learn about coring a glacier.", heading: "How are ice cores collected?", - paragraph2: "The ideal location for drilling an ice core is the highest, flattest section of a glacier, as this region minimizes any influence from the glacier flow. Snow builds up layer by layer on a glacier surface and eventually compresses into ice. High, flat drilling locations increase the possibility that ice layers remain horizontal and decreases the possibility of melt layers.", - paragraph3: "Once researchers have arrived at the drilling location, the first thing that they do is to create some sort of shelter for comfort and incase the weather becomes bad. For short (~10 m) ice cores that can be drilled in a day, this shelter is often a snow pit with a bench, as well as a tarp to block blowing wind. Once the pit is dug, scientists can start drilling. The ice core drill has bits called “core dogs†on the bottom that cut into the ice. The drill is a metal cylinder with threads on the outside that help grip the snow and ice. Researchers attach a handle to the top and turn the drill until the have collected a meter of ice. This meter of ice is then passed to the people in the snow pit who are processing the core. This processing entails documenting the ice stratigraphy, measuring the core, and determining its mass. On the Juneau Icefield, cores were not kept in a frozen state, and instead were homogenized and placed into clean labeled bottles. The drilling and processing continue simultaneously, with team members measuring one core section while the next section is being drilled. The ice core drill returns to the initial hole and retrieves the next meter of ice. As the drill goes deeper and deeper into the ice, metal rods are connected to the top of the drill to extend the drill length until eventually reaching ~10 m depth. Pulling up 10 m of metal extension rods, the drill, and the ice core section can be heavy, but allows for a continuous climate record.", - photo010: "Clear, sunny days on the Juneau Icefield are appreciated, and people spend every possible moment outside to enjoy the spectacular scenery.", - photo010Mobile: "Clear, sunny days on the Juneau Icefield are appreciated, and people spend every possible moment outside to enjoy the spectacular scenery.", + paragraph2: "The ideal location for drilling a snow core to identify biomarkers from wildfire events is the highest, flattest section of a glacier, as this region minimizes any influence from the glacier flow. Snow builds up layer by layer on a glacier surface and eventually compresses into ice. High, flat drilling locations increase the possibility that packed layers remain horizontal.", + paragraph3: "Once researchers have arrived at the drilling location, the first thing that they do is create some sort of shelter for comfort in case the weather becomes bad. For short (~10 m) snow cores that can be drilled in a day, this shelter is often a snow pit with a bench and a tarp to block wind. Once the pit is dug, scientists can start drilling.", + paragraph4: "Snow core drills either have a cutting head (like a Felix corer) or cutting teeth (like a Kovacs corer). The teeth cut the packed snow. Parts called ‘core dogs’ grab the core so that you can retrieve it from the bottom of the drilled hole. The drill itself is a metal cylinder and has threads on the outside that help excavate the cut core, allowing the drill to keep cutting deeper. Researchers attach a handle to the top and turn the drill until they have collected a meter of packed snow. This meter of snow is then passed to the people in the snow pit who are processing the core. This processing entails documenting the snow stratigraphy, measuring the core, and weighing the section to determine its mass.", + paragraph5: "The drilling and processing continue simultaneously, with team members measuring one core section while the next section is being drilled. The snow core drill returns to the initial hole and retrieves the next meter of packed snow. As the drill goes deeper and deeper into the glacier, metal extension rods are connected to the top of the drill to extend the drill length until eventually reaching ~10 meters below the surface.", + photo010: "The Juneau Icefield Research Program (JIRP) established a series of camps across the icefield that allow students and faculty the opportunity to live and work on the icefield throughout the summer program. Clear, sunny days on the Juneau Icefield are rare and much appreciated, and researchers spend every possible moment outside enjoying the spectacular scenery.", + photo010Mobile: "Clear, sunny days on the Juneau Icefield are rare and much appreciated, and researchers spend every possible moment outside enjoying the scenery.", photo010Alt: "", - photo018: "Drilling and processing ice cores involves teamwork. While a new core section is being drilled, team members measure physical properties like ice stratigraphy and the mass of the previous core section. Processing the core in a snow pit allows shelter from the wind, and creates a stable bench for examining the cores.", - photo018Mobile: "While a new core section is being drilled, team members measure physical properties like ice stratigraphy and the mass of the previous section.", + photo018: "Drilling and processing snow cores involves teamwork. While a new core section is being drilled, team members measure physical properties (e.g., volume, mass) and stratigraphy (i.e., the ice layering) of the previous core section. Processing the core in a snow pit allows shelter from the wind and creates a stable bench for examining the cores.", + photo018Mobile: "While a new core section is being drilled, team members measure physical properties (e.g., volume, mass) and stratigraphy (i.e., the ice layering) of the previous core section.", photo018Alt: "", - photo051: "Drilling an ice core on the Lemon Creek glacier to help determine past melt events affecting the glacier mass balance. The ice core drill can obtain one meter of ice at a time, where poles help extend the drill as it goes deeper into the ice. Team members record the depth and stratigraphy of ice core sections.", - photo051Mobile: "The ice core drill can obtain one meter of ice at a time, where poles help extend the drill as it goes deeper into the ice.", + photo051: "Scientist sample seasonal snow and firn (snow older than one year) on Lemon Creek Glacier, measuring density, noting any layers of interest, and collecting samples for chemical analysis. The coring device can obtain one meter of snow and firn at a time, and by adding extensions to the drill, it is possible to carefully sample many meters deep without having to dig an enormous hole. Team members take careful notes to ensure records are complete.", + photo051Mobile: "The coring device can obtain one meter of snow and firn at a time, and by adding extensions to the drill, it is possible to carefully sample many meters deep without having to dig an enormous hole.", photo051Alt: "", - photo085: "Sunsets on the Juneau Icefield are spectacular, with the combination of jagged peaks and glowing snow.", - photo085Mobile: "Sunsets on the Juneau Icefield are spectacular, with the combination of jagged peaks and glowing snow.", + photo085: "Sunsets on the Juneau Icefield are spectacular, with the combination of jagged peaks and low light gleaming on the snow.", + photo085Mobile: "Sunsets on the Juneau Icefield are spectacular, with the combination of jagged peaks and low light gleaming on the snow.", photo085Alt: "", - photo138: "Traversing between camps involves a combination of skiing across glaciers and hiking over slopes. By the time that teams are ready to traverse to a new camp, they are well-practiced in safety skills such as crevasse rescue and rope management.", - photo138Mobile: "Moving between camps involves skiing across glaciers and hiking over slopes. Teams practice safety skills like rope management and crevasse rescue.", + photo138: "Travel between sites on the icefield involves a combination of skiing across glaciers and hiking over slopes. In order to be able to traverse to a new camp, the teams must be well-practiced in safety skills such as crevasse rescue and rope management.", + photo138Mobile: "Travel between sites on the icefield involves a combination of skiing across glaciers and hiking over slopes. Teams use safety skills like crevasse rescue and rope management.", photo138Alt: "", - photo140: "Natalie Kehrwald takes a break from drilling ice cores on the Taku Glacier, one of the deepest temperate alpine glaciers in the world.", - photo140Mobile: "Natalie Kehrwald takes a break from drilling ice cores on the Taku Glacier, one of the deepest temperate alpine glaciers in the world.", + photo140: "Natalie Kehrwald takes a break from drilling snow cores on the Taku Glacier, one of the deepest temperate mountain glaciers in the world.", + photo140Mobile: "Natalie Kehrwald takes a break from drilling snow cores on the Taku Glacier, one of the deepest temperate mountain glaciers in the world.", photo140Alt: "", - photo156: "After multiple days without snowfall, a ski track forms from people passing through on their way to collect scientific data and explore the landscape.", - photo156Mobile: "After multiple days without snowfall, a ski track develops from people on their way to collect scientific data and explore the landscape.", + photo156: "Sunny summer days create a sun-cupped surface texture on the snow surface. The passage of scientists on skis and snowmobiles is visible here, showing a trail from camp to the coring site.", + photo156Mobile: "Sunny summer days create a sun-cupped surface texture on the snow surface. The passage of scientists on skis and snowmobiles is visible here.", photo156Alt: "", - photo183: "Crevasses form in a glacier where it stretches through extension or passes over obstacles. Traveling through crevasse fields requires caution. Crevasses near glacier edges are often substantially smaller, but probing ice bridges and crevasse depth is necessary. Traveling across most crevassed terrain requires rope teams.", - photo183Mobile: "Crevasses form in a glacier where it stretches through extension or passes over obstacles. Traveling through crevasse fields requires caution.", photo183Alt: "", - photo203: "Glaciers scour the landscape, creating U-shaped valleys between surrounding peaks. At their terminus, glacier melt creates lakes and rivers. The relatively low elevation and coastal location of the Juneau Icefield means that glacier melt mixes with seawater to create vibrant saltwater wetlands.", - photo203Mobile: "Glaciers scour the landscape, creating U-shaped valleys between surrounding peaks. At their terminus, glacier melt creates lakes and rivers.", + photo203: "Glaciers scour the landscape, creating U-shaped valleys between surrounding peaks. At their terminus (where they end), glacier melt creates lakes and rivers.", + photo203Mobile: "Glaciers scour the landscape, creating U-shaped valleys between surrounding peaks. Where they end, glacier melt creates lakes and rivers.", photo203Alt: "", - photo021: "The Gilkey Trench viewed from Camp 18 shows a classic example of ogives, or alternating bands of light and dark ice that are caused by compression and glacier flow.", - photo021Mobile: "The Gilkey Trench shows ogives, or alternating bands of light and dark ice that are caused by compression and glacier flow.", + photo021: "The movement of glaciers pushes sediments both in front of and to the sides of the ice. The mounds of sediments are called moraines and remain even after the ice has melted. Moraines provide evidence of past glacier activity in locations that are now free of ice.", + photo021Mobile: "The movement of glaciers pushes sediments both in front of and to the sides of the ice. The mounds of sediments are called moraines.", photo021Alt: "" }, RegionalFires: { - paragraph1: "From analysis of <a href='/visualizations/earth-in-flux/#/fire-in-ice/glacier-scan' target='_blank'>glacial ice cores</a>, we know that <a href='/visualizations/earth-in-flux/#/fire-in-ice/wildfire-aerosols' target='_blank'>wildfires burning softwoods have deposited aerosols</a> on the Juneau Ice Field. We also know that Alaska has many softwood forests, and that some have burned. But how can we tell which regional fires deposited aerosols on the glacier?", - paragraph2: "Researchers use an <a href='https://www.ready.noaa.gov/HYSPLIT.php' target='_blank'>atmospheric model</a> to trace the potential path of smoke particles generated by known wildfires, identifying 'candidate' fires that could be the source for aerosols deposited on the ice field during the sampled period." + paragraph1: "From analysis of <a href='/visualizations/earth-in-flux/#/fire-in-ice/glacier-scan' target='_blank'>glacier snow cores</a>, we know that wildfires burning softwoods like pine, fir, and spruce have <a href='/visualizations/earth-in-flux/#/fire-in-ice/wildfire-aerosols' target='_blank'>deposited aerosols</a> on the Juneau Icefield. We also know that Alaska has many softwood forests, and that some have burned. But how can we tell which regional fires deposited aerosols on the glacier?", + paragraph2: "Researchers use an Researchers use an atmospheric model (<a href='https://www.ready.noaa.gov/HYSPLIT.php' target='_blank'>HYSPLIT</a>) to trace the potential path of smoke particles generated by known wildfires, identifying 'candidate' fires that could be the source of aerosols deposited on the icefield during the sampled period." }, BeaufortSeaCore: { heading1: "Why collect ocean sediment cores?", @@ -199,9 +213,13 @@ export default { paragraph1: 'Explore total recreational harvest for the five families of inland fish with the largest recreational harvests: <span class="scientificName">Cyprinidae</span> (minnows and carps), <span class="scientificName">Percidae</span> (perch), <span class="scientificName">Salmonidae</span> (salmon, trout, grayling, and whitefish), <span class="scientificName">Bagridae</span> (bagrid catfish), and <span class="scientificName">Centrarchidae</span> (sunfishes). Total recreational harvest is broken out by family, by species, and by country. Hover over the chart to see the harvest totals, in kilograms' }, WildfireAerosols: { - paragraph1: "Each layer of the <a href='/visualizations/earth-in-flux/#/fire-in-ice/glacier-scan' target='_blank'>collected ice core</a> contains more than just ice. Particles from the air, called aerosols, deposit on the surface of the glacier. These aerosols can come from dust, fossil fuel combustion, or wildfires. When snow buries the deposited particles, they are preserved in the ice.", - paragraph2: "Can we tell if any of these particles came from wildfires? Three sugars — mannosan, galactosan, and levoglucosan — are only produced when vegetation burns. These sugars are present throughout the core, which tells us that some of the deposited particles in the ice were sourced from wildfires.", - paragraph3: "Scientists use the ratio of levoglucosan to the sum of mannosan and galactosan to distinguish between types of vegetation that burned. Alaska's forests are dominated by softwoods, and <a href='/visualizations/earth-in-flux/#/fire-in-ice/regional-fires' target='_blank'>regional fires</a> likely deposit aerosols on the Juneau Ice Field that are captured in the core. However, there are also markers of hardwood combustion, which suggests that aerosols are transported to the ice field from much farther afield. One possible source is wildfires in hardwood forests in East Asia." + paragraph1: "Each layer of the <a href='/visualizations/earth-in-flux/#/fire-in-ice/glacier-scan' target='_blank'>collected snow core</a> contains more than just packed snow. Particulates from the air, like dust, deposit on the surface of the glacier, along with tiny airborne particles called aerosols. Over time, the deposited particulates and aerosols are preserved in the glacier. If the aerosols are dark in color, the glacier absorbs more heat and melts more quickly. These dark aerosols include black carbon, or soot, that is generated when vehicles and industrial activities burn fossil fuels or when wildfires burn vegetation.", + paragraph2: "Can we tell if any of the dark aerosols in the snow core came from wildfires? While black carbon does not have a chemical signature, three sugars—mannnosan, galactosan, and levoglucosan—are only produced when vegetation burns. While these sugars are not dark aerosols themselves, they travel in smoke plumes with the dark aerosols and are deposited alongside them. These sugars are present throughout the core, which tells us that some of the deposited material in the snow came from wildfires.", + paragraph3: "Scientists use the ratio of levoglucosan to the sum of mannosan and galactosan to distinguish between types of vegetation that burned. Alaska's forests are dominated by <span class='tooltip-group'><span class='tooltip-span'>softwoods</span><span id='softwoods-tooltip' class='tooltiptext'>Conifers, like pine, spruce, and firs.</span></span>, and <a href='/visualizations/earth-in-flux/#/fire-in-ice/regional-fires' target='_blank'>regional fires</a> likely deposit aerosols on the Juneau Icefield that are captured in the core.", + paragraph4: "However, there are also markers of <span class='tooltip-group'><span class='tooltip-span'>hardwood</span><span id='hardwoods-tooltip' class='tooltiptext'>Broadleaved trees, like ash, oak, birch, beech, alder, and teak.</span></span> combustion, which suggests that wildfire aerosols are transported to the icefield from much farther afield. One possible source is wildfires from East Asia, where hardwood forests are more abundant.", + heading: "What am I looking at?", + explanation1: "As scientists <a href='/visualizations/earth-in-flux/#/fire-in-ice/glacier-scan' target='_blank'>collected the snow core</a>, they carefully stored each ten-centimeter section for transport off the Juneau Icefield. The retrieved samples were analyzed in a laboratory for particle counts, major ions, stable isotopes of oxygen and hydrogen, and the three sugars that are markers of biomass combustion—mannosan, galactosan, and levoglucosan.", + explanation2: "In this visual representation of the core, the darker grey shows layers of the snow that had high amounts of particulate matter. The high-particulate layer present in both 2015 and 2016 likely represents the summer melt surface." }, ThreatSankey: { paragraph1: 'Land use change is the biggest threat to inland fisheries.' diff --git a/src/components/ChartGrid.vue b/src/components/ChartGrid.vue index e5f7982e396551ce995f96dfe7c55759e574f809..767787e540d4e0bf0702749bdbb6abd6d5c8a94f 100644 --- a/src/components/ChartGrid.vue +++ b/src/components/ChartGrid.vue @@ -15,7 +15,7 @@ import { isMobile } from 'mobile-device-detect'; import ChartCard from '@/components/ChartCard.vue'; - import ChartGrid from '@/assets/content/ChartGrid.js'; + import ChartGridContent from '@/assets/content/ChartGrid.js'; const props = defineProps({ view: { @@ -27,7 +27,7 @@ // global variables const router = useRouter(); const mobileView = isMobile; - const chartContent = ChartGrid.chartGridItems; + const chartContent = ChartGridContent.chartGridItems; // set up filtered chart data as computed property const filteredChartContent = computed(() => { diff --git a/src/components/ChartIcon.vue b/src/components/ChartIcon.vue new file mode 100644 index 0000000000000000000000000000000000000000..9531934989afe3f16cc17109932c33d55404bde0 --- /dev/null +++ b/src/components/ChartIcon.vue @@ -0,0 +1,89 @@ +<template> + <div class="chart" :class="{ 'active': active }"> + <RouterLink :to="vizRoute ? vizRoute : './'"> + <img v-if="vizRoute" :src="src" :alt="alt" /> + <div v-if="projectRoute" id="overlay"> + <p class='overlay-text'>Project info</p> + </div> + </RouterLink> + </div> +</template> + +<script setup> + defineProps({ + src: { + type: String, + default: `` + }, + alt: { + type: String, + default: `` + }, + active: { + type: Boolean, + default: false + }, + vizRoute: { + type: String, + default: '' + }, + projectRoute: { + type: String, + default: '' + }, + }) +</script> + +<style scoped lang="scss"> + $card-height: 90px; + .chart{ + display: flex; + flex-direction: column-reverse; + justify-content: flex-start; + position: relative; + width: $card-height; + height: $card-height; + border-color: var(--faded-usgs-blue); + border-width: 0px; + border-style: solid; + border-radius: 1.5px; + box-shadow: 0px 0px 8px rgba(39,44,49,.07), 1px 4px 4px rgba(39,44,49,.04); + opacity: 0.5; + } + .chart img { + position: absolute; + width: 100%; + top: 0; + background-color: white; + border-radius: 1.5px; + } + + .active { + border-color: var(--faded-usgs-blue); + border-width: 3px; + border-style: solid; + border-radius: 1.5px; + opacity: 1; + transform: scale(1.1); + } + + .chart:hover { + opacity: 1; + transform: scale(1.1); + } + #overlay { + height: $card-height; + background-color: var(--faded-usgs-blue); + border-radius: 3px; + } + .overlay-text { + color: var(--usgs-blue); + text-align: center; + font-weight: 300; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 0; + } +</style> \ No newline at end of file diff --git a/src/components/ChartIconGrid.vue b/src/components/ChartIconGrid.vue new file mode 100644 index 0000000000000000000000000000000000000000..3f6bb73a29a4b9a20c0534e261c205aa7a287c91 --- /dev/null +++ b/src/components/ChartIconGrid.vue @@ -0,0 +1,63 @@ +<template> + <div id="chartGrid"> + <ChartIcon + :id="projectRoute" + :vizRoute=null + :projectRoute="projectRoute" + /> + <ChartIcon v-for="item in filteredChartContent" :key="item.vizRoute" + :id="item.vizRoute" + :src="getThumb(item.img_src)" + :alt="item.alt" + :active="item.vizRoute === vizRoute" + :vizRoute="item.vizRoute" + /> + </div> +</template> + +<script setup> + import { computed } from 'vue'; + + import ChartIcon from '@/components/ChartIcon.vue'; + import ChartGridContent from '@/assets/content/ChartGrid.js'; + + const props = defineProps({ + projectRoute: { + type: String, + default: `` + }, + vizRoute: { + type: String, + default: `` + }, + }) + + // global variables + const chartContent = ChartGridContent.chartGridItems; + + // set up filtered chart data as computed property + const filteredChartContent = computed(() => { + return chartContent.filter(d => d.project.replace(/\s+/g, '-').toLowerCase() === props.projectRoute).sort((a,b) => (a.chartOrder > b.chartOrder) ? 1 : ((b.chartOrder > a.chartOrder) ? -1 : 0)) + }); + + function getThumb(pic) { + return 'https://labs.waterdata.usgs.gov/visualizations/thumbnails/'+pic + } + +</script> + +<style> +#chartGrid{ + margin: 8rem auto 2rem auto; + display: flex; + justify-content: center; + gap: 50px; + flex-wrap: wrap; + width: 100%; + max-width: 70rem; + @media screen and (max-width: 600px) { + gap: 25px; + max-width: 95%; + } +} +</style> \ No newline at end of file diff --git a/src/components/GlacierScanViz.vue b/src/components/GlacierScanViz.vue index 0906dd60a6ed3366ef62b906ed4dcc3caad14aba..72dcda3a7210704b53c2b9e09a06b6fc378e93b2 100644 --- a/src/components/GlacierScanViz.vue +++ b/src/components/GlacierScanViz.vue @@ -14,7 +14,10 @@ <p v-html="text.paragraph1" /> <p v-html="text.promptDesktop" /> </div> - <p v-if="mobileView && defaultView" v-html="text.paragraph1Mobile" /> + <div v-if="mobileView && defaultView"> + <p v-html="text.paragraph1Mobile" /> + <p v-html="text.promptMobile" /> + </div> <p v-if="!defaultView" v-html="currentPhotoText"></p> </div> </div> @@ -34,6 +37,8 @@ <template #aboveExplanation> <p v-html="text.paragraph2" /> <p v-html="text.paragraph3" /> + <p v-html="text.paragraph4" /> + <p v-html="text.paragraph5" /> </template> </VizSection> </section> @@ -81,8 +86,8 @@ .attr("display", "none") crossSectionSVG.select("#tutorial-dt-2") .attr("display", "none") - // crossSectionSVG.select("#tutorial-mb-1") - // .attr("display", "none") + crossSectionSVG.select("#tutorial-mb-1") + .attr("display", "none") crossSectionSVG.select("#tutorial-mb-2") .attr("display", "none") crossSectionSVG.select("#legend_1") @@ -340,37 +345,39 @@ <style scoped lang="scss"> #cross-section-grid-container { display: flex; - flex-direction: column; - @media screen and (max-height: 770px) { - flex-direction: row; - } + flex-direction: row; + max-width: 1500px; + margin: 3rem auto 4rem auto; @media screen and (max-width: 600px) { flex-direction: column; } } #caption-container { - height: 15vh; - margin-top: 3rem; + height: 100%; + width: 35vw; + max-width: 600px; + margin: auto 3rem auto 0; display: flex; - flex-direction: row; + flex-direction: column; + flex-grow: 0; + flex-shrink: 0; align-items: center; - padding: 0rem 2rem 0 0rem; + padding: 0rem 3rem 2rem 3rem; background-color: var(--faded-usgs-blue); border-radius: 5px; box-shadow: 5px 5px 10px rgba(57, 61, 66, 0.2); font-style: italic; @media screen and (max-height: 770px) { - flex-direction: column; - height: 80vh; + height: 100%; width: 40vw; - padding: 0rem 2rem 0 2rem; - flex-grow: 0; - flex-shrink: 0; + max-width: 40vw; + padding: 0rem 2rem 1rem 2rem; } @media screen and (max-width: 600px) { flex-direction: column; width: 100%; - height: 40vh; + max-width: 100%; + height: 100%; padding: 1rem 1.5rem 0 1.5rem; } } @@ -379,26 +386,23 @@ pointer-events: none; border-radius: 5px; margin-right: 3rem; - @media screen and (max-height: 770px) { - max-width: 100%; - max-height: 45vh; - margin: 2rem 2rem 2rem 2rem; - } + max-width: 100%; + max-height: 45vh; + margin: 2rem 2rem 2rem 2rem; @media screen and (max-width: 600px) { padding-right: 0rem; - max-height: 22vh; + max-height: 35vh; max-width: 100%; margin: 1rem; } } #globe-image { width: 15vw; + max-width: 250px; align-items: center; pointer-events: none; border-radius: 5px; - @media screen and (max-height: 770px) { - margin: 4rem 2rem 2rem 2rem; - } + margin: 4rem 2rem 2rem 2rem; @media screen and (max-width: 600px) { width: 35vw; margin: 1rem; @@ -408,7 +412,7 @@ <style lang="scss"> /* css for elements added/classed w/ d3 */ #cross-section-svg { - height: 50vh; + height: 60vh; z-index: 1; margin-top: 3rem; @media screen and (max-height: 770px) { diff --git a/src/components/PreFooterCodeLinks.vue b/src/components/PreFooterCodeLinks.vue index 7d8f07ae4570ee5b9539dfd4841a9e96bc9cbc60..7fd27a2c8f9a130e5dc0f0cef502ce62fc562e82 100644 --- a/src/components/PreFooterCodeLinks.vue +++ b/src/components/PreFooterCodeLinks.vue @@ -33,6 +33,7 @@ color: var(--color-text); margin-left: 10px; text-decoration: none; + font-weight: 300; } } diff --git a/src/components/RegionalFiresViz.vue b/src/components/RegionalFiresViz.vue index 5a346ec6f5d7931781607a246953ade4a013e5b1..d8763a19ee286a0b1d0eddfdb48c804c72da5f20 100644 --- a/src/components/RegionalFiresViz.vue +++ b/src/components/RegionalFiresViz.vue @@ -47,7 +47,7 @@ onMounted(async () => { try { // Use external svg from s3 - d3.xml("https://labs.waterdata.usgs.gov/visualizations/svgs/regional_fires_map_v9.svg").then(function(xml) { + d3.xml("https://labs.waterdata.usgs.gov/visualizations/svgs/regional_fires_map_v10.svg").then(function(xml) { // add svg content to DOM const svgGrid = document.getElementById("aerosols-grid-container") svgGrid.appendChild(xml.documentElement); diff --git a/src/components/SubPage.vue b/src/components/SubPage.vue index 33aef0a8ffc7695977548b41cdc8b3e2ea68d62b..f22b1ab2a326a0710f79783fc3ac6a0604624ffc 100644 --- a/src/components/SubPage.vue +++ b/src/components/SubPage.vue @@ -11,19 +11,10 @@ {{ filteredChartContent.title }} </h1> </div> - <VizComponent :id="`${vizRoute}-viz`" :text="vizText"/> + <VizComponent :id="`${vizRoute}-viz`" :text="vizText" :key="$route.fullPath"/> + <ChartIconGrid :projectRoute="projectRoute" :vizRoute="vizRoute" :key="vizRoute"/> <ReferencesSection v-if="vizReferences" title="References" titleLevel="3" :references="vizReferences"/> <AuthorshipSection v-if="vizAuthors" title="" titleLevel="3" :authors="vizAuthors"/> - <div - class="text-container" - id="page-link-container" - > - <hr> - <p id="page-link-note">See the - <button class="project-link"><RouterLink :to="`/${projectRoute}`">{{ filteredChartContent.project }} project page</RouterLink></button> - for data sources and related visualizations. - </p> - </div> </div> <PreFooterCodeLinks :gitHubRepositoryLink="vizGitHubRepositoryLink"/> </section> @@ -32,9 +23,9 @@ <script setup> import { useRoute } from 'vue-router' import { isMobile } from 'mobile-device-detect'; - import { defineAsyncComponent } from 'vue' - - import ChartGrid from '@/assets/content/ChartGrid.js'; + import { computed, defineAsyncComponent, ref, shallowRef, watch } from 'vue' + import ChartIconGrid from '@/components/ChartIconGrid.vue'; + import ChartGridContent from '@/assets/content/ChartGrid.js'; import text from "@/assets/text/text.js"; import ReferencesSection from '@/components/ReferencesSection.vue'; import references from "@/assets/text/references"; @@ -44,53 +35,60 @@ // global variables const route = useRoute() - const vizRoute = route.params.vizRoute - const projectRoute = route.params.projectRoute + const vizRoute = ref(route.params.vizRoute); + const projectRoute = ref(route.params.projectRoute); const mobileView = isMobile; - const chartContent = ChartGrid.chartGridItems; - const filteredChartContent = chartContent.filter(d => d.vizRoute === vizRoute)[0] - const vizKey = filteredChartContent.vizKey - const vizText = text.visualizations[`${filteredChartContent.vizKey}`] - const vizReferences = references[`${filteredChartContent.vizKey}`] - const vizAuthors = authors[`${filteredChartContent.vizKey}`] + const VizComponent = shallowRef( + defineAsyncComponent(() => + //Use convention of adding Viz at end of component names to ensure are two words + //Plus, also allows us to specify a filename pattern here, per: + //https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#imports-to-your-own-directory-must-specify-a-filename-pattern + import(`./${vizKey.value}Viz.vue`) // + ) + ); + const chartContent = ChartGridContent.chartGridItems; const gitHubRepositoryLink = import.meta.env.VITE_APP_GITHUB_REPOSITORY_LINK; - const vizGitHubRepositoryLink = `${gitHubRepositoryLink}/blob/main/src/components/${vizKey}Viz.vue` //Use convention of adding Viz at end of component names to ensure are two words - const VizComponent = defineAsyncComponent(() => - //Use convention of adding Viz at end of component names to ensure are two words - //Plus, also allows us to specify a filename pattern here, per: - //https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#imports-to-your-own-directory-must-specify-a-filename-pattern - import(`./${vizKey}Viz.vue`) // - ) + // set up computed properties based on ref values + const filteredChartContent = computed(() => { + return chartContent.filter(d => d.vizRoute === vizRoute.value)[0]; + }); + + const vizKey = computed(() => { + return filteredChartContent.value.vizKey; + }); + + const vizText = computed(() => { + return text.visualizations[`${filteredChartContent.value.vizKey}`] + }); + + const vizReferences = computed(() => { + return references[`${filteredChartContent.value.vizKey}`] + }); + + const vizAuthors = computed(() => { + return authors[`${filteredChartContent.value.vizKey}`] + }); + + const vizGitHubRepositoryLink = computed(() => { + return `${gitHubRepositoryLink}/blob/main/src/components/${vizKey.value}Viz.vue` //Use convention of adding Viz at end of component names to ensure are two words + }); + + //watches router params for changes + watch(route, () => { + // update project route + projectRoute.value = route.params.projectRoute; + // update viz route + vizRoute.value = route.params.vizRoute; + // update dynamic component + VizComponent.value = defineAsyncComponent(() => + //Use convention of adding Viz at end of component names to ensure are two words + //Plus, also allows us to specify a filename pattern here, per: + //https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#imports-to-your-own-directory-must-specify-a-filename-pattern + import(`./${vizKey.value}Viz.vue`) // + ) + }) </script> <style scoped lang="scss"> - #page-link-container { - font-weight: 300; - font-style: italic; - } - #page-link-note { - margin-top: 2.5rem; - } - .project-link { - font-family: sans-serif; /* This is fallback font for old browsers */ - font-family: var(--default-font); - font-style: italic; - background-color: var(--faded-usgs-blue); - color: var(--usgs-blue); - border: solid var(--faded-usgs-blue); - border-radius: 5px; - font-weight: 300; - margin: 1rem 0.25rem 1rem 0.25rem; - padding: 0.3rem 0.5rem 0.5rem 0.5rem; - box-shadow: 3px 3px 3px rgba(39,44,49,.2); - } - .project-link a { - text-decoration: none; - color: var(--usgs-blue); - } - .project-link:hover { - box-shadow: rgba(39,44,49,.7) 2px 2px 4px -2px; - transform: translate3d(0, 2px, 0); - } </style> \ No newline at end of file diff --git a/src/components/WildfireAerosolsViz.vue b/src/components/WildfireAerosolsViz.vue index 380b883b439ccd718e0bbc7519990072cd836450..f82437675b9e7b11752a8d5a3e74dddcc71e129e 100644 --- a/src/components/WildfireAerosolsViz.vue +++ b/src/components/WildfireAerosolsViz.vue @@ -5,28 +5,40 @@ :figures="true" :fig-caption="false" > - <template #heading> - <h2> - {{ text.heading }} - </h2> - </template> <template #figures> <div id="wildfire-aerosols-grid-container"> - <button id="aerosol-prev" class="flip-button" @click="currentIndex--; clicked()" :disabled="isFirstImage || justClicked"> + <button id="aerosol-prev-upper" class="flip-button" @click="currentIndex--; clicked()" :disabled="isFirstImage || justClicked"> <font-awesome-icon :icon="{ prefix: 'fas', iconName: 'arrow-left' }" class="fa fa-arrow-left"/> </button> - <button id="aerosol-next" class="flip-button" @click="currentIndex++; clicked()" :disabled="isLastImage || justClicked"> + <button id="aerosol-next-upper" class="flip-button" @click="currentIndex++; clicked()" :disabled="isLastImage || justClicked"> <font-awesome-icon :icon="{ prefix: 'fas', iconName: 'arrow-right' }" class="fa fa-arrow-right"/> </button> <div id="aerosol-text-container" class="text-container"> <p v-html="currentText" /> </div> <div id="chart-container" ref="chart"></div> + <button v-if="!mobileView" id="aerosol-prev-lower" class="flip-button" @click="currentIndex--; clicked()" :disabled="isFirstImage || justClicked"> + <font-awesome-icon :icon="{ prefix: 'fas', iconName: 'arrow-left' }" class="fa fa-arrow-left"/> + </button> + <button v-if="!mobileView" id="aerosol-next-lower" class="flip-button" @click="currentIndex++; clicked()" :disabled="isLastImage || justClicked"> + <font-awesome-icon :icon="{ prefix: 'fas', iconName: 'arrow-right' }" class="fa fa-arrow-right"/> + </button> </div> </template> - <template #figureCaption> + </VizSection> + <VizSection + id="cross-section" + :figures="true" + :fig-caption="false" + > + <template #heading> + <h2> + {{ text.heading }} + </h2> </template> - <template #belowExplanation> + <template #aboveExplanation> + <p v-html="text.explanation1" /> + <p v-html="text.explanation2" /> </template> </VizSection> </template> @@ -53,16 +65,17 @@ const scatterData = ref(); const currentIndex = ref(1); const justClicked = ref(false); - const nIndices = 3; + const nIndices = 4; const chart = ref(null); let chartSVG; - const chartTitle = 'Title of chart'; + const chartTitle = 'Series of charts depicting particulate counts and the presence of wildfire biomarkers in layers of a 780-centimeter snow core'; let chartHeight; let chartWidth; let chartDimensions; let chartBounds; let chartGap; let maskingRect; + let annotationGap; let tileChartTranslateX1; let tileChartTranslateX2; let tileChartTranslateX3; @@ -90,7 +103,7 @@ let scatterChartBounds; let scatterXScale; let scatterColorCategories; - const scatterColors = {grass: '#c49051', hardwood: '#3c475a', softwood: '#729C9D'}; + const scatterColors = {hardwood: '#c49051', softwood: '#729C9D'}; let scatterColorScale; const transitionLength = 1000; @@ -123,21 +136,22 @@ // initialize chart elements // on desktop, don't let chart height exceed 800px - const desktopHeight = window.innerHeight < 770 ? window.innerHeight * 0.85 : Math.min(window.innerHeight * 0.75, 800); + const desktopHeight = window.innerHeight < 770 ? window.innerHeight * 1.05 : Math.min(window.innerHeight * 0.75, 800); chartHeight = mobileView ? window.innerHeight * 0.6 : desktopHeight; chartWidth = chart.value.offsetWidth; initChart({ width: chartWidth, height: chartHeight, - margin: mobileView ? 5 : 30 + margin: mobileView ? 5 : 5, + marginLeft: mobileView ? 5 : 30 }) const defaultMargin = mobileView ? 5 : 10; - const sharedTopMargin = mobileView ? 135 : 130; + const sharedTopMargin = mobileView ? 135 : 165; const sharedBottomMargin = mobileView ? 0 : 10; chartGap = mobileView ? chartDimensions.boundedWidth / 11 : chartDimensions.boundedWidth / 11; - const tileChartWidth = mobileView ? chartGap * 3 : chartGap * 3; + const tileChartWidth = mobileView ? chartGap * 3 : chartGap * 2.5; const barChartWidth = mobileView ? chartGap * 3 : chartGap * 3; const scatterChartWidth = mobileView ? chartGap * 2 : chartGap * 2; @@ -156,7 +170,7 @@ }); barChartTranslateX2 = mobileView ? tileChartTranslateX2 + tileChartWidth + chartGap : tileChartTranslateX2 + tileChartWidth + chartGap; - barChartTranslateX3 = mobileView ? tileChartTranslateX3 + tileChartWidth + chartGap : tileChartTranslateX3 + tileChartWidth + chartGap; + barChartTranslateX3 = mobileView ? tileChartTranslateX3 + tileChartWidth + chartGap * 0.75 : tileChartTranslateX3 + tileChartWidth + chartGap; initBarChart({ width: barChartWidth, height: chartHeight, @@ -191,9 +205,9 @@ .attr("width", barChartDimensions.width + scatterChartDimensions.width) .attr("height", chartHeight) .style("transform", `translate(${ - tileChartTranslateX1 + tileChartDimensions.width + chartGap * 0.5 + tileChartTranslateX1 + tileChartDimensions.width + chartGap }px, 0px)`); - drawScatterChart(scatterData.value); + drawScatterChart(scatterData.value, 'softwood'); scatterChartWrapper .attr("visibility", "hidden"); @@ -338,10 +352,10 @@ initYAxis({bounds: tileChartBounds}) // Add groups for visual elements - tileChartBounds.append("g") - .attr("class", "rects"); tileChartBounds.append("g") .attr("class", "annotations"); + tileChartBounds.append("g") + .attr("class", "rects"); } function initBarChart({ @@ -664,7 +678,7 @@ function initTileColorScale() { tileColorScale = d3.scaleSequential() - .interpolator(d3.interpolateGreys); + .range(["#efefef" ,"#000000"]); } function initBarColorScale(data) { @@ -724,8 +738,7 @@ //////////////////////////////////// ///// ADD CHART ELEMENTS ///// //////////////////////////////////// - const annotationGap = tileChartDimensions.boundedWidth * 0.5; - const annotationBuffer = annotationGap * 0.2; + annotationGap = tileChartDimensions.boundedWidth * 0.5; // draw chart tileChartBounds.select('.rects') // selects our group we set up to hold chart elements .selectAll(".rect") // empty selection @@ -740,44 +753,39 @@ .attr("width", tileChartDimensions.boundedWidth - annotationGap) .style("fill", d => tileColorScale(colorAccessor(d))); - // draw year bands + // Add horizontal black line to differentiate years tileChartBounds.select(".annotations") - .append("rect") - .attr("class", "year-bands") - .attr("x", annotationGap / 2 + annotationBuffer) - .attr("y", 0) - .attr("height", yScale(372)) - .attr("width", 3) - - tileChartBounds.select(".annotations") - .append("rect") - .attr("class", "year-bands") - .attr("x", annotationGap / 2 + annotationBuffer) - .attr("y", yScale(378)) - .attr("height", tileChartDimensions.boundedHeight - yScale(378)) - .attr("width", 3) - + .append("line") + .attr("x1", 0) + .attr("x2", chartDimensions.boundedWidth) + .attr("y1", yScale(370)) + .attr("y2", yScale(370)) + .style("stroke", "#000000") + .style("stroke-width", 1) + .style("stroke-dasharray", ("2, 5")); + + // Add year labels tileChartBounds.select(".annotations") .append("text") - .attr("class", "axis-text") - .attr("x", - yScale(372) / 2) - .attr("y", annotationGap / 2) - .attr("transform", "rotate(-90)") + .attr("class", "axis-title") + .attr("y", yScale(365)) + .attr("x", annotationGap / 2) .attr("text-anchor", "middle") - .text("2016 accumulation") - + .attr("dominant-baseline", "text-after-edge") + .text("2016") + tileChartBounds.select(".annotations") .append("text") - .attr("class", "axis-text") - .attr("x", - yScale(378) - ((tileChartDimensions.boundedHeight - yScale(378)) / 2)) - .attr("y", annotationGap / 2) - .attr("transform", "rotate(-90)") + .attr("class", "axis-title") + .attr("y", yScale(375)) + .attr("x", annotationGap / 2) .attr("text-anchor", "middle") - .text("2015 accumulation") + .attr("dominant-baseline", "text-before-edge") + .text("2015") } function addTileLegend() { - // build list of posible counts (0 to 366) + // build list of possible total particle counts let count_list = []; for (let i = 1; i <= tileColorScale.domain()[1]; i++) { count_list.push(i); @@ -804,13 +812,17 @@ .attr("class", "axis-title") .attr("x", tileChartDimensions.boundedWidth / 2) .attr("y", -tileChartDimensions.margin.top) + .attr("dx", 0) + .attr("dy", 0) .attr("text-anchor", "middle") .attr("dominant-baseline", "text-before-edge") + .attr("text-width", tileChartDimensions.boundedWidth) .text("Particulate count") + .call(d => wrap(d)) // append legend rectangle const rectWidth = tileChartDimensions.boundedWidth / 2; - const rectHeight = mobileView ? tileChartDimensions.margin.top / 6 : tileChartDimensions.margin.top / 4; + const rectHeight = mobileView ? tileChartDimensions.margin.top / 8 : tileChartDimensions.margin.top / 6; const rectX = tileChartDimensions.boundedWidth / 2 - rectWidth / 2; legendGroup.append("rect") .attr("class", "c1p2 matrixLegend") @@ -964,7 +976,7 @@ .attr("y", -barChartDimensions.margin.top / 1.75) .attr("text-anchor", "start") // left-align text .attr("dominant-baseline", "central") - .text(d => d); + .text(d => d.toLowerCase()); // Position legend groups // https://stackoverflow.com/questions/20224611/d3-position-text-element-dependent-on-length-of-element-before @@ -1013,7 +1025,8 @@ // Determine x and y translation // set y translation for each row - let rowHeight = window.innerHeight < 770 ? legendRectSize * 4 : legendRectSize * 2; + let rowHeight = window.innerHeight < 600 ? legendRectSize * 4.5 : legendRectSize * 3; + rowHeight = mobileView ? legendRectSize * 4.5 : rowHeight; const yTranslation = rowHeight * i; // let yTranslation = 0; // if (!mobileView) { @@ -1043,10 +1056,11 @@ }) } - function drawScatterChart(data) { + function drawScatterChart(data, type) { ////////////////////////////// ///// PROCESS DATA ///// ////////////////////////////// + const filteredData = data.filter(d => d.vegetation_type == type) /////////////////////////////////////////// ///// SET UP ACCESSOR FUNCTIONS ///// @@ -1066,7 +1080,7 @@ /////////////////////////////////////////// // set domain for xScale scatterXScale - .domain([... new Set(data.map(d => xAccessor(d)))]); + .domain([... new Set(filteredData.map(d => xAccessor(d)))]); /////////////////////////////////// ///// SET UP COLOR SCALE ///// @@ -1078,18 +1092,64 @@ ///// ADD CHART ELEMENTS ///// //////////////////////////////////// // draw chart - const desktopPointSize = window.innerHeight < 770 ? 2 : 4; scatterChartBounds.select('.points') // selects our group we set up to hold chart elements .selectAll(".point") // empty selection - .data(data) // bind data - .enter() // instantiate chart element for each element of data - .append("circle") // append a rectangle for each element - .attr("class", "point") - .attr("id", d => 'point-' + identifierAccessor(d)) - .attr("cx", d => scatterXScale(xAccessor(d)) + scatterXScale.bandwidth()/2) - .attr("cy", d => barYScale(yAccessor(d)) + barYScale.bandwidth()/2) - .attr("r", mobileView ? 2 : desktopPointSize) - .style("fill", d => scatterColorScale(colorAccessor(d))); + .data(filteredData, d => d.vegetation_type) // bind data + .join( + enter => enter + .append("circle") + .attr("class", "point") + .attr("id", d => 'point-' + identifierAccessor(d)) + .attr("cx", d => scatterXScale(xAccessor(d)) + scatterXScale.bandwidth()/2) + .attr("cy", d => barYScale(yAccessor(d)) + barYScale.bandwidth()/2) + .attr("r", barYScale.bandwidth() / 2 * 0.95) + .style("fill", d => scatterColorScale(colorAccessor(d))) + .style("fill-opacity", 0) + .transition() + .duration(transitionLength) + .style("fill-opacity", 1), + + null, // no update function + + exit => { + exit + .transition() + .duration(transitionLength) + .style("fill-opacity", 0) + .remove(); + } + ); + + const baseWidth = mobileView ? scatterChartTranslateX3 - scatterChartDimensions.boundedWidth + (barYScale.bandwidth() / 2 * 0.95 * 2) : scatterChartTranslateX3 - scatterChartDimensions.boundedWidth / 2 + (barYScale.bandwidth() / 2 * 0.95 * 2); + scatterChartBounds.select('.points') // selects our group we set up to hold chart elements + .selectAll(".rect") // empty selection + .data(filteredData, d => d.vegetation_type) // bind data + .join( + enter => enter + .append("rect") + .attr("class", "rect") + .attr("id", d => 'point-' + identifierAccessor(d)) + .attr("x", d => scatterXScale(xAccessor(d)) + scatterXScale.bandwidth()/2) + .attr("y", d => barYScale(yAccessor(d))) + .attr("height", barYScale.bandwidth()) + .attr("width", d => scatterXScale(xAccessor(d)) + baseWidth) //(chartDimensions.boundedWidth - scatterChartDimensions.boundedWidth - chartGap) + .attr("transform", d => "translate(" + - (baseWidth + scatterXScale(xAccessor(d))) + ", 0)") + .style("fill", d => scatterColorScale(colorAccessor(d))) + .style("opacity", 0) + .transition() + .duration(transitionLength) + .style("opacity", 0.5), + + null, // no update function + + exit => { + exit + .transition() + .duration(transitionLength) + .style("fill-opacity", 0) + .remove(); + } + ); } function addScatterLegend() { @@ -1110,8 +1170,7 @@ .text('Burned vegetation type') .call(d => wrap(d)) - const desktopPointSize = window.innerHeight < 770 ? 2 : 4; - const legendPointSize = mobileView ? 2 : desktopPointSize; + const legendPointSize = barYScale.bandwidth() / 2 * 0.95; // const interItemSpacing = mobileView ? 15 : 10; const intraItemSpacing = 6; @@ -1132,7 +1191,7 @@ legendGroups.append("circle") .attr("class", "legend-point") .attr("cx", 0) - .attr("cy", -scatterChartDimensions.margin.top / 1.75 + legendPointSize / 1.5) + .attr("cy", -scatterChartDimensions.margin.top / 2 + legendPointSize / 1.5) .attr("r", legendPointSize) .style("fill", d => scatterColorScale(d)) @@ -1140,7 +1199,7 @@ legendGroups.append("text") .attr("class", "legend-text") .attr("x", legendPointSize + intraItemSpacing) // put text to the right of the rectangle - .attr("y", -scatterChartDimensions.margin.top / 1.75) + .attr("y", -scatterChartDimensions.margin.top / 2) .attr("text-anchor", "start") // left-align text .attr("dominant-baseline", "central") .text(d => d); @@ -1191,8 +1250,8 @@ // Determine x and y translation // set y translation for each row - let rowHeight = window.innerHeight < 770 ? barYScale.bandwidth() * 4 : barYScale.bandwidth() * 2; - const yTranslation = rowHeight * i; + let rowHeight = window.innerHeight < 600 ? barYScale.bandwidth() * 4.5 : barYScale.bandwidth() * 3; const yTranslation = rowHeight * i; + rowHeight = mobileView ? barYScale.bandwidth() * 4.5 : rowHeight; // let yTranslation = 0; // if (!mobileView) { // if (i < 2) { @@ -1267,7 +1326,7 @@ } else if (index == 2) { maskingRect .style("transform", `translate(${ - tileChartTranslateX2 + tileChartDimensions.width + barChartDimensions.width + chartGap * 2 + tileChartTranslateX2 + tileChartDimensions.width + barChartDimensions.width + chartGap * 2.25 }px, 0px)`) .transition() .duration(transitionLength) @@ -1281,11 +1340,18 @@ } if (!scatterChartHidden) hideChart(scatterChartWrapper) } else if (index == 3) { + drawScatterChart(scatterData.value, 'softwood') maskingRect .style("opacity", "0") moveChart(tileChartWrapper, tileChartTranslateX3) moveChart(barChartWrapper, barChartTranslateX3) - showChart(scatterChartWrapper, scatterChartTranslateX3) + if (scatterChartHidden) { + showChart(scatterChartWrapper, scatterChartTranslateX3) + } + } else if (index == 4) { + maskingRect + .style("opacity", "0") + drawScatterChart(scatterData.value, 'hardwood') } } @@ -1306,16 +1372,9 @@ dx = parseFloat(text.attr("dx")), tspan = text.text(null).append("tspan").attr("y", y).attr("dy", dy + "em"); - - console.log(`wrap: ${words}`) while ((word = words.pop())) { line.push(word); tspan.text(line.join(" ")); - console.log(`width: ${width}`) - console.log(`word: ${word}`) - console.log(tspan.node()) - console.log(tspan.node().getComputedTextLength()) - console.log(tspan.node().getComputedTextLength() > width) if (tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(" ")); @@ -1354,9 +1413,9 @@ grid-template-columns: 10% calc(80% - 4rem) 10%; grid-template-rows: auto max-content; grid-template-areas: - "prev text next" - "chart chart chart"; - margin: 2rem auto 0 auto; + "prev-upper text next-upper" + "prev-lower chart next-lower"; + margin: 2rem auto 4rem auto; column-gap: 2rem; row-gap: 3rem; @media only screen and (max-width: 600px) { @@ -1364,7 +1423,7 @@ grid-template-rows: auto max-content; grid-template-areas: "chart chart chart" - "prev text next"; + "prev-upper text next-upper"; } } #chart-container { @@ -1373,7 +1432,8 @@ } #aerosol-text-container { grid-area: text; - height: 15vh; + height: 19vh; + align-content: center; @media screen and (max-height: 770px) { height: 30vh; } @@ -1390,20 +1450,26 @@ cursor: pointer; box-shadow: 0px 0px 4px rgba(39,44,49,.3); @media only screen and (max-width: 600px) { - height: 3rem; - width: 3rem; + height: 3.5rem; + width: 3.5rem; align-self: start; } } - #aerosol-prev { - grid-area: prev; + #aerosol-prev-upper { + grid-area: prev-upper; justify-self: end; - align-self: start; } - #aerosol-next { - grid-area: next; + #aerosol-next-upper { + grid-area: next-upper; + justify-self: start; + } + #aerosol-prev-lower { + grid-area: prev-lower; + justify-self: end; + } + #aerosol-next-lower { + grid-area: next-lower; justify-self: start; - align-self: start; } button:hover:after { top: 0px; @@ -1438,7 +1504,13 @@ } </style> <style lang="scss"> -/* css for elements added/classed w/ d3 */ + #softwoods-tooltip { + margin-left: -145px; + } + #hardwoods-tooltip { + margin-left: -145px; + } + /* css for elements added/classed w/ d3 */ #masking-rect { fill: var(--color-background); } diff --git a/src/views/VisualizationView.vue b/src/views/VisualizationView.vue index 563adeb0aaaa948820d574af0008e898da73fc72..405cacff90c47f60da662291bc2ef699bd1b088a 100644 --- a/src/views/VisualizationView.vue +++ b/src/views/VisualizationView.vue @@ -80,7 +80,7 @@ <h3>About the {{ pageText.title }} research</h3> </template> <template #aboveExplanation> - <p v-html="pageText.motivation" /> + <p v-for="paragraph, item in pageText.motivation" :key="item" v-html="paragraph" /> </template> </VizSection> <VizSection