<template> <section id="beeswarm"> <div id="text1" class="text-container"> <p> Everyone needs access to clean water. Water insecurity is influenced by a number of social vulnerability indicators. This includes <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; background: #f9f9f9; padding: 5px; border: 1px solid #ccc; border-radius: 5px;"></div> </div> </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 = 800; 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)})`) .text("Inconclusive"); // Set forces const forceY = d3.forceY(d => yScale(d.level_agreement)).strength(0.5); const forceX = d3.forceX(margin.left + (width / 2)).strength(0.5); const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 5).iterations(20); const forceManyBody = d3.forceManyBody().strength(-5); 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); d3.select('#tooltip') .style('opacity', 1) .html(`<strong>${d.determinant}</strong><br>appeared in ${d.evidence_val} studies`) .style('left', (x + 10) + 'px') .style('top', (y - 28) + 'px'); 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, 90]); // 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(); // Update existing bubbles bubbles .attr('r', d => radiusScale(d.evidence_val)) .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]); // Add new bubbles bubbles.enter() .append('circle') .attr('class', 'bubble') .attr('r', d => radiusScale(d.evidence_val)) .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]) .merge(bubbles) // Merge to apply forces to new and existing bubbles .attr('cx', d => d.x) // Use existing x position .attr('cy', d => d.y); // Restart simulation with new data simulation.nodes(dataPoints) .force('x', forceX) .force('y', forceY) .force('collide', forceCollide) .force('charge', forceManyBody) .alpha(0.2) .restart(); } </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: #f9f9f9; padding: 5px; border: 15px solid black; border-radius: 15px; pointer-events: none; /* Prevent tooltip from blocking mouse events */ } </style>