<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 models where increased <b>consensus</b> indicates a majority of models using the selected determinant recorded the same direction of influence on conditions of water insecurity and <b>inconclusive</b> indicates models using the selected determinant did not record the same direction of influence on conditions of water insecurity.</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)) .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>