<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> <div id="legend-container"></div> <div id="text2" class="text-container"> <p><em>Many social vulnerability determinants have been studied. Some show positive relationships with water insecurity, some with negative, and others inconclusive (Drakes et al. 2024). Interact with the chart to see the level of agreement across studies.</em></p> </div> </section> </template> <script setup> import { onMounted, ref } 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" }; 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'] } ]; function createLegend() { const legend = d3.select('#legend-container') .append('svg') .attr('width', width) .attr('height', 50) .attr('class', 'legend'); const legendGroup = legend.selectAll('g') .data(legendData) .enter() .append('g') .attr('transform', (d, i) => `translate(${i * 100}, 0)`); legendGroup.append('rect') .attr('x', 15) .attr('y', 17) .attr('width', 15) .attr('height', 15) .attr('class', d => d.name.toLowerCase() + '-key') .style('fill', d => d.pattern ? d.color : d.color) .style('stroke', d => d.stroke ? d.stroke : 'none') .style('stroke-width', d => d.stroke ? 2 : 0); legendGroup.append('text') .attr('x', 35) .attr('y', 25) .attr('dy', '0.35em') //.style('font-size', '12px') .text(d => d.name); } // 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("Consensus"); 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', 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(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))); }); } createLegend(); // add legend to caption } 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'); // Set the y scale const yScale = d3.scaleLinear() .domain([40, d3.max(data.value, d => d.level_agreement)]) .range([height - 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([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(); } //tooltip 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 } const [x, y] = d3.pointer(event, svg.node()); const tooltip = d3.select('#tooltip'); tooltip.html('') .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: `url(#pattern-stripe)` } ]; // 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') .attr('stroke', dimensionColors[d.dimension.replace(' ', '')]) .attr('stroke-width', 3); 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(#${patternId})` : d.fill) .style('stroke', d => d.stroke ? d.stroke : 'none'); // Position the tooltip tooltip .style('opacity', 1) .style('left', (x + 10) + 'px') .style('top', (y - 28) + 'px'); // Highlight the circle d3.select(this) .attr('stroke', dimensionColors[d.dimension.replace(' ', '')]) .attr('stroke-width', 8); d3.select('#legend-container').select('.positive-key') .style('fill', dimensionColors[d.dimension.replace(' ', '')]); d3.select('#legend-container').select('.negative-key') .style('stroke', dimensionColors[d.dimension.replace(' ', '')]); d3.select('#legend-container').select('.unknown-key') //.select('path') .style('stroke', dimensionColors[d.dimension.replace(' ', '')]); // Show the legend // d3.select('#legend-container').style('display', 'block'); } function handleMouseOut() { d3.select('#tooltip').style('opacity', 0); d3.select(this) .attr('stroke', null) .attr('stroke-width', null); // Hide the legend //d3.select('#legend-container').style('display', 'none'); } </script> <style scoped lang="scss"> $switchWidth: 12rem; $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; } #beeswarm-chart-container svg { max-width: 100%; max-height: 100%; height: auto; /* Maintain aspect ratio */ display: inline-block; } .yLabel { font-weight: bold; } .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; } } #tooltip { position: absolute; opacity: 0; background: $ThemeGrey; padding: 2px; // border: 2px solid black; border-radius: 10px; pointer-events: none; /* Prevent tooltip from blocking mouse events */ } .hidden { display: none; } #legend-container { display: none; /* Hide the legend initially */ } #caption-container { margin-top: 20px; font-style: italic; margin-left: auto; margin-right: auto; } </style>