<template> <section id="beeswarm"> <div id="text1" class="text-container"> <p> Everyone needs access to clean water. The gap between water security and <span class="tooltip-group"><span class="tooltip-span"> water insecurity </span><span class="tooltiptext" > Populations cannot maintain access to adequate quantities of water at an acceptable quality to sustain livelihoods, development, and human and ecosystem health.</span></span> can be the difference between a daydream and a nightmare. People may be more or less vulnerable to water insecurity due to <span :class="['highlight', 'Demographiccharacteristics', { checked: isChecked.Demographiccharacteristics }]" @click="toggleCategory('Demographiccharacteristics')" > demographic characteristics </span>, <span :class="['highlight', 'Health', { checked: isChecked.Health }]" @click="toggleCategory('Health')" > health </span>, <span :class="['highlight', 'Livingconditions', { checked: isChecked.Livingconditions }]" @click="toggleCategory('Livingconditions')" > living conditions </span>, <span :class="['highlight', 'Socioeconomicstatus', { checked: isChecked.Socioeconomicstatus }]" @click="toggleCategory('Socioeconomicstatus')" > socioeconomic status </span>, <span :class="['highlight', 'Riskperception', { checked: isChecked.Riskperception }]" @click="toggleCategory('Riskperception')" > risk perception </span>, <span :class="['highlight', 'Landtenure', { checked: isChecked.Landtenure }]" @click="toggleCategory('Landtenure')" > land tenure </span>, and <span :class="['highlight', 'Exposure', { checked: isChecked.Exposure }]" @click="toggleCategory('Exposure')" > exposure to stressors </span> (like drought or pollution). Below is an chart that displays how certain social vulnerability determinants relate to water insecurity. </p> </div> <div id="beeswarm-chart-container"> </div> <div id="text2" class="text-container tooltip-width"> <div id="tooltip" class="tooltip">Interact with the chart to explore evidence<br>for social vulnerability determinants.</div> <em>Many social vulnerability determinants have been studied. Some show positive <span class="legend-box positive"></span> relationships with water insecurity, some negative <span class="legend-box negative"></span>, and others unknown <span class="legend-box unknown"></span> <a href='https://www.sciencebase.gov/catalog/item/63f79d49d34e4f7eda456572' target='_blank'>(Hines and others, 2023)</a>. The y-axis of the chart represents the level of agreement among studies where increased <b>consensus</b> indicates a majority of studies using the selected determinant recorded the same direction of influence on conditions of water insecurity and <b>inconclusive</b> indicates studies using the selected determinant did not record the same direction of influence on conditions of water insecurity. The size of the bubbles on the chart represents the number of studies, with larger bubbles indicating that a particular determinant has been studied more frequently.</em> </div> </section> </template> <script setup> import { onMounted, ref } from "vue"; import * as d3 from 'd3'; import { isMobile } from 'mobile-device-detect'; // Global variables const publicPath = import.meta.env.BASE_URL; const dataSet1 = ref([]); const dataSet2 = ref([]); const selectedDataSet = ref('dataSet1'); const data = ref([]); let simulation; // Set up SVG let svg; const height = 600; const width = 800; const margin = { top: 40, right: 20, bottom: 40, left: 40 }; const isChecked = ref({ Demographiccharacteristics: true, Health: true, Livingconditions: true, Socioeconomicstatus: true, Riskperception: true, Landtenure: true, Exposure: true }); // Set colors for bubble charts const dimensionColors = { Demographiccharacteristics: "#092836", Landtenure: "#1b695e", Livingconditions: "#7a5195", Socioeconomicstatus: "#2a468f", Health: "#ef5675", Riskperception: "#ff764a", Exposure: "#ffa600" }; const patternId = 'pattern-stripe'; // bar chart patterning and fills const legendData = [ { name: 'Positive', color: dimensionColors['Demographiccharacteristics'] }, { name: 'Negative', color: 'white', stroke: dimensionColors['Demographiccharacteristics'] }, { name: 'Unknown', pattern: true, color: `url(#pattern-stripe)`, stroke: dimensionColors['Demographiccharacteristics'] } ]; // create legend for stack bar charts function createPattern() { const svgDefs = d3.select('body') .append('svg') .attr('width', 0) .attr('height', 0) .append('defs'); const pattern = svgDefs.append('pattern') .attr('id', patternId) .attr('patternUnits', 'userSpaceOnUse') .attr('width', 8) .attr('height', 8); pattern.append('rect') .attr('width', 8) .attr('height', 8) .attr('fill', 'white'); pattern.append('path') .attr('d', 'M-2,2 l4,-4 M0,8 l8,-8 M6,10 l4,-4') .attr('stroke', dimensionColors['Demographiccharacteristics']) .attr('stroke-width', 3); } function createLegend() { const keyW = 65; const keyH = 12; createPattern(); legendData.forEach(d => { const legend = d3.select(`.${d.name.toLowerCase()}`) .append('svg') .attr('width', keyW + 5) .attr('height', keyH); legend.append('rect') .attr('x', 4) .attr('y', 0) .attr('width', keyW) .attr('height', keyH) .style('fill', d.pattern ? d.color : d.color) .style('stroke', d.stroke ? d.stroke : 'none') .style('stroke-width', d.stroke ? 2 : 0); }); } // Load data and then make chart onMounted(async () => { try { await loadDatasets(); data.value = selectedDataSet.value === 'dataSet1' ? dataSet1.value : dataSet2.value; if (data.value.length > 0) { createBeeswarmChart(); createLegend(); } else { console.error('Error loading data'); } } catch (error) { console.error('Error during component mounting', error); } }); async function loadDatasets() { try { dataSet1.value = await loadData('determinant_uncertainty.csv'); dataSet2.value = await loadData('indicator_uncertainty.csv'); 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.level_agreement = +(+d.level_agreement).toFixed(2); d.evidence_val = +d.evidence_val; d.sig_value = +d.sig_value; return d; }); return data; } catch (error) { console.error(`Error loading data from ${fileName}`, error); return []; } } function createBeeswarmChart() { // Remove any existing SVG element d3.select('#beeswarm-chart-container').select('svg').remove(); // Get dynamic dimensions to draw chart const containerWidth = document.getElementById('beeswarm-chart-container').offsetWidth; const containerHeight = window.innerWidth <= 700 ? window.innerHeight * 0.5 : 600; const margin = window.innerWidth <= 700 ? { top: 40, right: 10, bottom: 20, left: 10 } : { top: 40, right: 20, bottom: 40, left: 40 }; const width = containerWidth - margin.left - margin.right; const height = containerHeight - margin.top - margin.bottom; svg = d3 .select('#beeswarm-chart-container') .append('svg') .attr('class', 'beeswarmSvg') .attr('viewBox', `0 0 ${containerWidth} ${containerHeight}`) .attr('preserveAspectRatio', 'xMidYMid meet') .style('width', '100%') .style('height', 'auto'); const yScale = d3.scaleLinear() .domain([40, d3.max(data.value, d => d.level_agreement)]) .range([containerHeight - margin.bottom, margin.top]); // Set radius based on evidence value const radiusScale = d3.scaleLinear() .domain([d3.min(data.value, d => d.evidence_val), d3.max(data.value, d => d.evidence_val)]) .range(window.innerWidth <= 700 ? [8, 40] : [10, 70]); const yAxis = svg.append('g') .attr("transform", "translate(50, 0)") .call(d3.axisLeft(yScale) .ticks(5) .tickFormat(d => d + '%')) .attr("stroke-width", 2) .attr("font-size", 18); // Add label to y axis svg.append('text') .attr("class", "yLabel") .attr("text-anchor", "left") .attr("font-weight", 700) .attr("transform", `translate(${margin.left}, ${margin.top / 2})`) .text("Consensus"); svg.append('text') .attr("class", "yLabel") .attr("text-anchor", "left") .attr("font-weight", 700) .attr("transform", `translate(${margin.left}, ${containerHeight - (margin.bottom / 2) + 10})`) .text("Inconclusive"); // Set forces const forceY = d3.forceY(d => yScale(d.level_agreement)).strength(0.9); const forceX = d3.forceX(margin.left + (width / 2)).strength(0.1); const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 2).iterations(20); const forceManyBody = d3.forceManyBody().strength(1); const bubbles = svg .selectAll('.bubble') .data(data.value) .enter() .append('circle') .attr('class', 'bubble') .attr('r', d => radiusScale(d.evidence_val)) .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]) .on('mouseover', handleMouseOver) .on('mouseout', handleMouseOut); // Run simulation simulation = d3.forceSimulation() .force('x', forceX) .force('y', forceY) .force('collide', forceCollide) .force('charge', forceManyBody) .nodes(data.value) .on('tick', ticked) .alpha(0.75) .alphaDecay(0.03); function ticked() { bubbles .attr("cx", d => Math.max(margin.left + radiusScale(d.evidence_val), Math.min(width - margin.right - radiusScale(d.evidence_val), d.x))) .attr("cy", d => Math.max(radiusScale(d.evidence_val), Math.min(containerHeight - radiusScale(d.evidence_val), d.y))); } //createLegend(); // add legend to caption } function toggleCategory(category) { isChecked.value[category] = !isChecked.value[category]; console.log(`Category toggled: ${category}, new value: ${isChecked.value[category]}`); updateChart(); } function updateChart() { // Adjust height based on screen size const isMobile = window.innerWidth <= 700; const containerHeight = isMobile ? window.innerHeight * 0.50 : height; // Set the y scale const yScale = d3.scaleLinear() .domain([40, d3.max(data.value, d => d.level_agreement)]) .range([containerHeight - margin.bottom, margin.top]); // Set the radius scale based on evidence value const radiusScale = d3.scaleLinear() .domain([d3.min(data.value, d => d.evidence_val), d3.max(data.value, d => d.evidence_val)]) .range(window.innerWidth <= 700 ? [8, 40] : [10, 70]); // Filter data based on active categories const activeCategories = Object.keys(isChecked.value).filter(category => isChecked.value[category]); console.log('Active categories:', activeCategories); // Select all bubbles and bind data const bubbles = svg.selectAll(".bubble") .data(data.value, d => d.id); // Update existing bubbles bubbles .attr('r', d => radiusScale(d.evidence_val)) .style('fill', d => activeCategories.includes(d.dimension.replace(' ', '')) ? dimensionColors[d.dimension.replace(' ', '')] : 'rgba(217, 217, 217, 0.95)'); // Add new bubbles bubbles.enter() .append('circle') .attr('class', 'bubble') .attr('r', d => radiusScale(d.evidence_val)) .style('fill', d => activeCategories.includes(d.dimension.replace(' ', '')) ? dimensionColors[d.dimension.replace(' ', '')] : 'rgba(217, 217, 217, 0.95)') .attr('cx', d => d.x) // Use existing x position .attr('cy', d => d.y) .on('mouseover', handleMouseOver) .on('mouseout', handleMouseOut) .merge(bubbles); // Ensure all bubbles are handled correctly bubbles.exit().remove(); } function updatePatternColor(color) { d3.select(`#${patternId} path`) .attr('stroke', color); } // Tooltip functions function handleMouseOver(event, d) { const activeCategories = Object.keys(isChecked.value).filter(category => isChecked.value[category]); // Check if the category of the data point is toggled if (!activeCategories.includes(d.dimension.replace(' ', ''))) { return; // Do nothing if the category is untoggled } // Add text to tooltip const tooltip = d3.select('#tooltip'); tooltip.html('') .append('div') .html(`<strong>${d.determinant_wrapped}</strong> appeared in ${d.evidence_val} ${d.evidence_val === 1 ? 'study' : 'studies'}.`); // Define categories in stacked bar chart const barData = [ { name: 'positive', value: d.pos_related_total, stroke: dimensionColors[d.dimension.replace(' ', '')], fill: dimensionColors[d.dimension.replace(' ', '')] }, { name: 'negative', value: d.neg_related_total, stroke: dimensionColors[d.dimension.replace(' ', '')], fill: 'white' }, { name: 'unknown', value: d.unk_direction_total, pattern: true, stroke: dimensionColors[d.dimension.replace(' ', '')], fill: `url(#pattern-stripe)` } ]; // Set dimensions for the bar chart const barWidth = 200; const barHeight = 15; // Create an SVG element for the bar chart const svgBar = tooltip.append('svg') .attr('width', barWidth + 10) .attr('height', barHeight + 10); const g = svgBar.append('g') .attr('transform', 'translate(8, 8)'); // Create a scale for the x-axis const xBar = d3.scaleLinear() .domain([0, d3.sum(barData, d => d.value)]) .range([0, barWidth]); // Create groups for each bar segment const barGroups = g.selectAll('g') .data(barData) .enter() .append('g'); // Add the rectangles for the bars barGroups.append('rect') .attr('x', (d, i) => i > 0 ? xBar(d3.sum(barData.slice(0, i), d => d.value)) : 0) .attr('y', 0) .attr('width', d => xBar(d.value)) .attr('height', barHeight) .style('fill', d => d.pattern ? `url(#${patternId})` : d.fill) .style('stroke', d => d.stroke ? d.stroke : 'none'); updatePatternColor(dimensionColors[d.dimension.replace(' ', '')]); // add emphasis to bubbles d3.select(this) .attr('stroke', dimensionColors[d.dimension.replace(' ', '')]) .attr('stroke-width', 8); // update colors in legend d3.select('#text2').select('.positive').select('rect') .style('fill', dimensionColors[d.dimension.replace(' ', '')]); d3.select('#text2').select('.negative').select('rect') .style('fill', 'white') .style('stroke', dimensionColors[d.dimension.replace(' ', '')]); d3.select('#text2').select('.unknown').select('rect') .style('fill', `url(#pattern-stripe)`) .style('stroke', dimensionColors[d.dimension.replace(' ', '')]); d3.select('#text2').select(`#${patternId} path`) //.style('fill', `url(#pattern-stripe)`) .attr('stroke', dimensionColors[d.dimension.replace(' ', '')]); } function handleMouseOut() { const tooltip = d3.select('#tooltip'); // add starting text tooltip.html('Interact with the chart to explore evidence for<br>social vulnerability determinants.'); // remove emphasis on bubbles d3.select(this) .attr('stroke', null) .attr('stroke-width', null); /* d3.select('#text2').select('.positive').select('rect') .style('fill', dimensionColors['Demographiccharacteristics']); d3.select('#text2').select('.negative').select('rect') .style('fill', 'white') .style('stroke', dimensionColors['Demographiccharacteristics']); */ /* d3.select('#text2').select('.unknown').select('rect') .style('fill', `url(#pattern-stripe)`) .style('stroke', dimensionColors[d.dimension.replace(' ', '')]); */ } </script> <style scoped lang="scss"> $Demographiccharacteristics: #092836; $Landtenure: #1b695e; $Livingconditions: #7a5195; $Socioeconomicstatus: #2a468f; $Health: #ef5675; $Riskperception: #ff764a; $Exposure: #ffa600; $ThemeGrey: rgba(217, 217, 217, 0.95); #beeswarm-chart-container { text-align: center; position: relative; max-width: 800px; margin: auto; } #beeswarm-chart-container svg { //width: 90vw; max-width: 700px; max-height: 100%; height: auto; /* Maintain aspect ratio */ margin: auto; display: inline-block; } .yLabel { font-weight: bold; } .tooltip-width { width: 100%; margin: 0 auto; display: flex; flex-direction: column; align-items: center; } .tooltip { opacity: 1; background: $ThemeGrey; padding: 8px; border-radius: 5px; pointer-events: none; width: 100%; height: 70px; text-align: center; } .hidden { display: none; } .highlight { color: white; padding: 0.25px 5px; border-radius: 10px; white-space: nowrap; font-weight: bold; cursor: pointer; transition: all 0.1s; &.Demographiccharacteristics { background-color: $Demographiccharacteristics; } &.Landtenure { background-color: $Landtenure; } &.Livingconditions { background-color: $Livingconditions; } &.Socioeconomicstatus { background-color: $Socioeconomicstatus; } &.Health { background-color: $Health; } &.Riskperception { background-color: $Riskperception; } &.Exposure { background-color: $Exposure; } &:not(.checked) { color: black; background-color: $ThemeGrey; } } @media (max-width: 700px) { .yLabel { font-size: 12px; /* Adjusted font size for mobile devices */ } #beeswarm-chart-container { width: 100vw; /* Make the chart container take up 100% of the viewport width on mobile */ height: auto; /* Make the chart container take up the full height of the screen */ } #beeswarm-chart-container svg { height: 100%; /* Make the SVG fill the container height */ } #mobile-tooltip { display: block; /* Show the mobile tooltip only on mobile devices */ margin: 10px; } } </style> <style lang="scss"> .tooltip-span { position: relative; cursor: pointer; display: inline-block; border-bottom: 1px dotted rgba(54, 54, 54, 0.8); z-index: 10; } .tooltiptext { visibility: hidden; width: 200px; background-color: #555; color: #fff; text-align: center; border-radius: 6px; padding: 5px; position: absolute; z-index: 1; margin-left: -170px; margin-top: 2rem; opacity: 0; transition: opacity 0.3s; } .tooltip-group:hover .tooltiptext { visibility: visible; opacity: 1; } </style>