Skip to content
Snippets Groups Projects
BeeswarmChart.vue 10.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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')"
    
    Cee Nell's avatar
    Cee Nell committed
              >
                demographic characteristics
              </span>,
              <span 
    
                :class="['highlight', 'Health', { checked: isChecked.Health }]" 
                @click="toggleCategory('Health')"
    
    Cee Nell's avatar
    Cee Nell committed
              >
                health
              </span>, 
              <span 
    
                :class="['highlight', 'Livingconditions', { checked: isChecked.Livingconditions }]" 
                @click="toggleCategory('Livingconditions')"
    
    Cee Nell's avatar
    Cee Nell committed
              >
                living conditions
              </span>, 
              <span 
    
                :class="['highlight', 'Socioeconomicstatus', { checked: isChecked.Socioeconomicstatus }]" 
                @click="toggleCategory('Socioeconomicstatus')"
    
    Cee Nell's avatar
    Cee Nell committed
              >
                socioeconomic status
              </span>,
              <span 
    
                :class="['highlight', 'Riskperception', { checked: isChecked.Riskperception }]" 
                @click="toggleCategory('Riskperception')"
    
    Cee Nell's avatar
    Cee Nell committed
              >
                risk perception
              </span>,
              <span 
    
                :class="['highlight', 'Landtenure', { checked: isChecked.Landtenure }]" 
                @click="toggleCategory('Landtenure')"
    
    Cee Nell's avatar
    Cee Nell committed
              >
                land tenure
              </span>, and 
              <span 
    
                :class="['highlight', 'Exposure', { checked: isChecked.Exposure }]" 
                @click="toggleCategory('Exposure')"
    
    Cee Nell's avatar
    Cee Nell committed
              >
                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>
    
    Cee Nell's avatar
    Cee Nell committed
      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');
    
    Cee Nell's avatar
    Cee Nell committed
          }
    
        } catch (error) {
          console.error('Error during component mounting', error);
    
    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');
          console.log('data in')
        } catch (error) {
          console.error('Error loading datasets', error);
    
    Cee Nell's avatar
    Cee Nell committed
        }
    
      }
      
      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 [];
    
    Cee Nell's avatar
    Cee Nell committed
        }
    
      // 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([30, d3.max(data.value, d => d.level_agreement)])
          .range([height-margin.bottom, margin.top]);
    
    Cee Nell's avatar
    Cee Nell committed
        
    
        // 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]);
      
        const yAxis = svg.append('g')
    
          .attr("transform", "translate(100, 0)")
    
    Cee Nell's avatar
    Cee Nell committed
          .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")
    
    Cee Nell's avatar
    Cee Nell committed
        .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")
    
    Cee Nell's avatar
    Cee Nell committed
          .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(1);
    
        const forceX = d3.forceX(width / 2).strength(0.3);
    
        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))
          .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)
          .alpha(0.2);
      
    
          function ticked() {
            bubbles
    
              .attr("cx", d => Math.max(50+radiusScale(d.evidence_val), Math.min(width - 50 - 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([30, 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))
    
    Cee Nell's avatar
    Cee Nell committed
          .style('fill', d => dimensionColors[d.dimension.replace(' ', '')])
          .merge(bubbles)  // Merge to apply forces to new and existing bubbles
    
    Cee Nell's avatar
    Cee Nell committed
          .attr('cx', d => d.x)  // Use existing x position
    
    Cee Nell's avatar
    Cee Nell committed
          .attr('cy', d => d.y);
    
      
        // Restart simulation with new data
    
    Cee Nell's avatar
    Cee Nell committed
        simulation.nodes(dataPoints)
    
    Cee Nell's avatar
    Cee Nell committed
          .force('x', forceX)
    
    Cee Nell's avatar
    Cee Nell committed
          .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;
      }
      </style>