<template> <section id="beeswarm"> <div id="text1" class="text-container"> <p> Everyone needs access to clean water. People may be more 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). </p> </div> <div id="beeswarm-chart-container"> <div id="tooltip" style="position: absolute; opacity: 0;"></div> </div> <caption> A meta-analysis by Drakes et al. (2024) evaluated how water insecurity in the Western U.S. is influenced by social determinants. Interact with the chart and the highlighted text above to see what they found. </caption> </section> </template> <script setup> import { onMounted, ref, watch } from "vue"; import * as d3 from 'd3'; // 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: 50, right: 20, bottom: 50, left: 50 }; 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" }; // 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(); } 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 []; } } // make beeswarm of determinants function createBeeswarmChart() { svg = d3 .select('#beeswarm-chart-container') .append('svg') .attr('class', 'beeswarmSvg') .attr('width', width) .attr('height', height); const yScale = d3.scaleLinear() .domain([40, d3.max(data.value, d => d.level_agreement)]) .range([height-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([10, 70]); const yAxis = svg.append('g') .attr("transform", "translate(100, 0)") .call(d3.axisLeft(yScale).ticks(5)) .attr("stroke-width", 2) .attr("font-size", 20); // 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("Level of Agreement"); svg.append('text') .attr("class", "yLabel") .attr("text-anchor", "left") .attr("font-weight", 700) .attr("transform", `translate(${margin.left}, ${height - (margin.bottom/2) + 10})`) .text("Inconclusive"); // Set forces const forceY = d3.forceY(d => yScale(d.level_agreement)).strength(0.7); const forceX = d3.forceX(margin.left + (width / 2)).strength(0.2); 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', function (event, d) { const [x, y] = d3.pointer(event); const tooltip = d3.select('#tooltip') tooltip.html('') tooltip.append('div') .html(`<strong>${d.determinant}</strong><br>appeared in ${d.evidence_val} ${d.evidence_val === 1 ? 'study' : 'studies'}`); // add 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: 'white' } ]; // Set dimensions for the bar chart const barWidth = 130; const barHeight = 10; // Create an SVG element for the bar chart const svgBar = tooltip.append('svg') .attr('width', barWidth + 10) .attr('height', barHeight + 10); const defs = svgBar.append('defs'); const pattern = defs.append('pattern') .attr('id', 'pattern-stripe') .attr('patternUnits', 'userSpaceOnUse') .attr('width', 8) // Adjusted to make pattern larger .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') // Adjusted path for thicker stripes .attr('stroke', dimensionColors[d.dimension.replace(' ', '')]) .attr('stroke-width', 3); // Thicker stripes 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 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(#pattern-stripe)' : d.fill) .style('stroke', d => d.stroke ? d.stroke : 'none'); // Position the tooltip tooltip .style('opacity', 1) .attr('stroke', dimensionColors[d.dimension.replace(' ', '')]) .style('left', (x + 10) + 'px') .style('top', (y - 28) + 'px'); // Highlight the circle d3.select(this) .attr('stroke', d => dimensionColors[d.dimension.replace(' ', '')]) .attr('stroke-width', 15); }) .on('mouseout', function () { d3.select('#tooltip').style('opacity', 0); d3.select(this) .attr('stroke', null) .attr('stroke-width', null); }); // 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(height - radiusScale(d.evidence_val), d.y))) //.each(d => { d.y = Math.max(radiusScale(d.evidence_val), Math.min(height - radiusScale(d.evidence_val), yScale(d.level_agreement))); }); } } function toggleCategory(category) { //console.log(`Toggle category called for: ${category}`); isChecked.value[category] = !isChecked.value[category]; console.log(`Category toggled: ${category}, new value: ${isChecked.value[category]}`); updateChart(); } function updateChart() { console.log('Update chart called'); const yScale = d3.scaleLinear() .domain([40, d3.max(data.value, d => d.level_agreement)]) .range([height-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([10, 70]); // Filter data based on active categories const activeCategories = Object.keys(isChecked.value).filter(category => isChecked.value[category]); const dataPoints = data.value.filter(d => activeCategories.includes(d.dimension.replace(' ', ''))); console.log('Active categories:', activeCategories); console.log('Filtered data points:', dataPoints); // Update existing bubbles and add new bubbles const bubbles = svg.selectAll(".bubble") .data(dataPoints, d => d.id); // Remove old bubbles bubbles.exit().remove(); // Add new bubbles bubbles.enter() .append('circle') .attr('class', 'bubble') .attr('r', d => d3.select('.bubble').size() ? d3.select('.bubble').attr('r') : radiusScale(d.evidence_val)) .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]) .attr('cx', d => d.x) // Use existing x position .attr('cy', d => d.y) .merge(bubbles) // Merge to apply forces to new and existing bubbles .attr('r', d => radiusScale(d.evidence_val)) .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]); } </script> <style scoped lang="scss"> $switchWidth: 12rem; $Demographiccharacteristics: #092836; $Landtenure: #1b695e; $Livingconditions: #7a5195; $Socioeconomicstatus: #2a468f; $Health: #ef5675; $Riskperception: #ff764a; $Exposure: #ffa600; #beeswarm-chart-container { text-align: center; position: relative; } #beeswarm-chart-container svg { max-width: 100%; max-height: 100%; height: auto; /* Maintain aspect ratio */ display: inline-block; } .bubble { stroke: black; stroke-width: 2px; fill-opacity: 0.8; } .chart-text { user-select: none; } .yLabel { font-weight: bold; } .highlight { color: white; padding: 0.25px 5px; border-radius: 10px; white-space: nowrap; font-weight: bold; cursor: pointer; /* Add cursor pointer for better UX */ transition: all 0.1s; /* Smooth transition for background color and border */ } .highlight:not(.checked) { background-color: white; border: 2px solid; } .highlight.Demographiccharacteristics { background-color: $Demographiccharacteristics; } .highlight.Demographiccharacteristics:not(.checked) { color: $Demographiccharacteristics; border-color: $Demographiccharacteristics; } .highlight.Landtenure { background-color: $Landtenure; } .highlight.Landtenure:not(.checked) { color: $Landtenure; border-color: $Landtenure; } .highlight.Livingconditions { background-color: $Livingconditions; } .highlight.Livingconditions:not(.checked) { color: $Livingconditions; border-color: $Livingconditions; } .highlight.Socioeconomicstatus { background-color: $Socioeconomicstatus; } .highlight.Socioeconomicstatus:not(.checked) { color: $Socioeconomicstatus; border-color: $Socioeconomicstatus; } .highlight.Health { background-color: $Health; } .highlight.Health:not(.checked) { color: $Health; border-color: $Health; } .highlight.Riskperception { background-color: $Riskperception; } .highlight.Riskperception:not(.checked) { color: $Riskperception; border-color: $Riskperception; } .highlight.Exposure { background-color: $Exposure; } .highlight.Exposure:not(.checked) { color: $Exposure; border-color: $Exposure; } #tooltip { position: absolute; opacity: 0; background: rgba(250, 250, 250, 0.93); padding: 2px; // border: 2px solid black; border-radius: 10px; pointer-events: none; /* Prevent tooltip from blocking mouse events */ } </style>