Skip to content
Snippets Groups Projects
BeeswarmChart.vue 10.1 KiB
Newer Older
Cee Nell's avatar
Cee Nell committed
<template>
Cee Nell's avatar
Cee Nell committed
    <section id="beeswarm">
Cee Nell's avatar
Cee Nell committed
      <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', 'exposureToStressors', { checked: isChecked.exposureToStressors }]" 
            @click="toggleCategory('exposureToStressors')"
          >
            exposure to stressors
          </span> (like drought or pollution).
        </p>
      </div>
      <div id="beeswarm-chart-container"></div>
Cee Nell's avatar
Cee Nell committed
    </section>
Cee Nell's avatar
Cee Nell committed
  </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 = 800;
    const width = 800;
    const margin = { top: 30, right: 20, bottom: 20, left: 30 };
    
    const isChecked = ref({
      demographicCharacteristics: true,
      health: true,
      livingConditions: true,
      socioeconomicStatus: true,
      riskPerception: true,
      landTenure: true,
      exposureToStressors: 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();
Cee Nell's avatar
Cee Nell committed
        } else {
Cee Nell's avatar
Cee Nell committed
          console.error('Error loading data');
Cee Nell's avatar
Cee Nell committed
        }
Cee Nell's avatar
Cee Nell committed
      } catch (error) {
        console.error('Error during component mounting', error);
      }
Cee Nell's avatar
Cee Nell committed
    });
Cee Nell's avatar
Cee Nell committed
    
    async function loadDatasets() {
      try {
        dataSet1.value = await loadData('determinant_uncertainty.csv');
        dataSet2.value = await loadData('indicator_uncertainty.csv');
      } 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() {
      svg = d3
        .select('#beeswarm-chart-container')
        .append('svg')
        .attr('class', 'beeswarmSvg')
        .attr('width', width)
Cee Nell's avatar
Cee Nell committed
        .attr('height', height);
    
      const yScale = d3.scaleLinear()
        .domain([40, d3.max(data.value, d => d.level_agreement)])
Cee Nell's avatar
Cee Nell committed
        .range([margin.bottom + height, 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]);
Cee Nell's avatar
Cee Nell committed
    
      const yAxis = svg.append('g')
        .attr("transform", "translate(50, 0)")
        .call(d3.axisLeft(yScale));
    
      // Add label to y axis
      svg.append('text')
Cee Nell's avatar
Cee Nell committed
        .attr("class", "yLabel")
        .attr("text-anchor", "start")
        .attr("x", 10)
        .attr("y", 20)
        .text("High level of Agreement");
Cee Nell's avatar
Cee Nell committed
    
      svg.append('text')
Cee Nell's avatar
Cee Nell committed
        .attr("class", "yLabel")
        .attr("text-anchor", "start")
        .attr("x", 10)
Cee Nell's avatar
Cee Nell committed
        .attr("y", height - 50)
        .text("Inconclusive");
    
Cee Nell's avatar
Cee Nell committed
      // Set forces
      const forceY = d3.forceY(d => yScale(d.level_agreement)).strength(1);
      const forceX = d3.forceX(width / 1.5).strength(0.1);
      const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 2);
      const forceManyBody = d3.forceManyBody().strength(-15);
    
      const bubbles = svg
        .selectAll('.bubble')
        .data(data.value)
        .enter()
        .append('circle')
        .attr('class', 'bubble')
        .attr('r', d => radiusScale(d.evidence_val))
Cee Nell's avatar
Cee Nell committed
        .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]);
    
      // Run simulation
      simulation = d3.forceSimulation()
        .force('x', forceX)
        .force('y', forceY)
        .force('collide', forceCollide)
        .force('charge', forceManyBody)
        .nodes(data.value)
        .on('tick', ticked)
Cee Nell's avatar
Cee Nell committed
        .alpha(0.2);
    
      function ticked() {
Cee Nell's avatar
Cee Nell committed
          .attr("cx", d => d.x)
          .attr("cy", d => d.y);
      }
    }
    
    function toggleCategory(category) {
      isChecked.value[category] = !isChecked.value[category];
      updateChart();
    }
    
    function updateChart() {
      const yScale = d3.scaleLinear()
        .domain([30, d3.max(data.value, d => d.level_agreement)])
        .range([margin.bottom + height, 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(' ', '')));
    
      // 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))
        .attr('cx', d => d.x)
        .attr('cy', d => d.y)
        .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]);
    
      // Add new bubbles
      bubbles.enter()
        .append('circle')
        .attr('class', 'bubble')
        .attr('r', d => radiusScale(d.evidence_val))
        .attr('cx', () => Math.random() * (width - margin.left - margin.right) + margin.left)
        .attr('cy', d => yScale(d.level_agreement))
        .style('fill', d => dimensionColors[d.dimension.replace(' ', '')]);
    
      // Restart simulation with new data
      simulation.nodes(dataPoints).alpha(0.2).restart();
Cee Nell's avatar
Cee Nell committed
</script>
Cee Nell's avatar
Cee Nell committed

Cee Nell's avatar
Cee Nell committed
  
<style scoped lang="scss">
$switchWidth: 12rem;
Cee Nell's avatar
Cee Nell committed
$Demographiccharacteristics: #092836;
$Landtenure: #1b695e;
$Livingconditions: #7a5195;
$Socioeconomicstatus: #2a468f;
$Health: #ef5675;
$Riskperception: #ff764a;
$Exposure: #ffa600;
Cee Nell's avatar
Cee Nell committed

#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;
}
Cee Nell's avatar
Cee Nell committed
.yLabel {
    font-weight: bold;
}
Cee Nell's avatar
Cee Nell committed

.highlight {
    color: white;
    padding: 0.25px 5px;
    border-radius: 10px;
    white-space: nowrap;
Cee Nell's avatar
Cee Nell committed
    font-weight: bold;
Cee Nell's avatar
Cee Nell committed
    cursor: pointer; /* Add cursor pointer for better UX */
    transition: all 0.1s; /* Smooth transition for background color and border */
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed

.highlight:not(.checked) {
    background-color: white;
    border: 2px solid;
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed
.highlight.demographicCharacteristics {
    background-color: $Demographiccharacteristics;
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed
.highlight.demographicCharacteristics:not(.checked) {
    color: $Demographiccharacteristics;
    border-color: $Demographiccharacteristics;
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed
.highlight.landTenure {
    background-color: $Landtenure;
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed
.highlight.landTenure:not(.checked) {
    color: $Landtenure;
    border-color: $Landtenure;
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed
.highlight.livingConditions {
    background-color: $Livingconditions;
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed
.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.exposureToStressors {
    background-color: $Exposure;
}
.highlight.exposureToStressors:not(.checked) {
    color: $Exposure;
    border-color: $Exposure;
Cee Nell's avatar
Cee Nell committed
}
Cee Nell's avatar
Cee Nell committed
</style>