Skip to content
Snippets Groups Projects
BeeswarmChart.vue 13.3 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>
    
    Cee Nell's avatar
    Cee Nell committed
          <div id="beeswarm-chart-container">
    
    Cee Nell's avatar
    Cee Nell committed
            <div id="tooltip" style="position: absolute; opacity: 0;"></div>
    
    Cee Nell's avatar
    Cee Nell committed
          </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;
    
    Cee Nell's avatar
    Cee Nell committed
      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');
    
    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([40, 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)])
    
    Cee Nell's avatar
    Cee Nell committed
          .range([10, 70]);
    
      
        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)
    
    Cee Nell's avatar
    Cee Nell committed
          .attr("transform", `translate(${margin.left}, ${height - (margin.bottom/2) + 10})`)
    
          .text("Inconclusive");
    
      
        // Set forces
    
    Cee Nell's avatar
    Cee Nell committed
        const forceY = d3.forceY(d => yScale(d.level_agreement)).strength(0.7);
        const forceX = d3.forceX(margin.left + (width / 2)).strength(0.2);
    
    Cee Nell's avatar
    Cee Nell committed
        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))
    
    Cee Nell's avatar
    Cee Nell committed
          .style('fill', d => dimensionColors[d.dimension.replace(' ', '')])
          .on('mouseover', function (event, d) {
    
    Cee Nell's avatar
    Cee Nell committed
            const [x, y] = d3.pointer(event);
            const tooltip = d3.select('#tooltip')
    
    Cee Nell's avatar
    Cee Nell committed
            tooltip.html('')
            tooltip.append('div')
              //.style('opacity', 1)
    
    Cee Nell's avatar
    Cee Nell committed
              .html(`<strong>${d.determinant}</strong><br>appeared in ${d.evidence_val} ${d.evidence_val === 1 ? 'study' : 'studies'}`);
    
    Cee Nell's avatar
    Cee Nell committed
              //.style('left', (x + 10) + 'px')
              //.style('top', (y - 28) + 'px');
    
              // add stacked bar chart
              const barData = [
                { name: 'positive', value: d.pos_related_total },
                { name: 'negative', value: d.neg_related_total },
                { name: 'unknown', value: d.unk_related_total }
              ];
    
              // Set dimensions for the bar chart
              const barWidth = 100;
              const barHeight = 10;
    
              // Create an SVG element for the bar chart
              const svgBar = tooltip.append('svg')
                .attr('width', barWidth + 20)
                .attr('height', barHeight + 20);
    
              const g = svgBar.append('g')
                .attr('transform', 'translate(10, 10)');
    
              // 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, i) => d3.schemeCategory10[i]);
    
              // 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', d => dimensionColors[d.dimension.replace(' ', '')])
                .attr('stroke-width', 15);
          })
    
    Cee Nell's avatar
    Cee Nell committed
          .on('mouseout', function () {
            d3.select('#tooltip').style('opacity', 0);
    
            d3.select(this)
              .attr('stroke', null)
              .attr('stroke-width', null);
    
    Cee Nell's avatar
    Cee Nell committed
          });
    
      
        // 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.75)
          .alphaDecay(0.03);
    
          function ticked() {
            bubbles
    
    Cee Nell's avatar
    Cee Nell committed
              .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)))
    
    Cee Nell's avatar
    Cee Nell committed
              //.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)])
    
    Cee Nell's avatar
    Cee Nell committed
          .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')
    
    Cee Nell's avatar
    Cee Nell committed
          .attr('r', d => d3.select('.bubble').size() ? d3.select('.bubble').attr('r') : radiusScale(d.evidence_val))
    
    Cee Nell's avatar
    Cee Nell committed
          .style('fill', d => dimensionColors[d.dimension.replace(' ', '')])
    
    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)
          .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;
    
    Cee Nell's avatar
    Cee Nell committed
      background: white;
      padding: 2px;
      border: 2px solid black;
      border-radius: 5px;
    
      pointer-events: none; /* Prevent tooltip from blocking mouse events */
    }