Skip to content
Snippets Groups Projects
BeeswarmChart.vue 16.9 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>
    
    Cee Nell's avatar
    Cee Nell committed
              Everyone needs access to clean water. People may be more vulnerable to water insecurity due to  
    
    Cee Nell's avatar
    Cee Nell committed
              <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
          <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>
    
    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 } 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"
      };
    
    Cee Nell's avatar
    Cee Nell committed
    
      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 definePattern(svg) {
        const defs = svg.append('defs');
    
        const pattern = defs.append('pattern')
          .attr('id', patternId)
          .attr('patternUnits', 'userSpaceOnUse')
          .attr('width', 8)
          .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['Demographiccharacteristics'])
          .attr('stroke-width', 3);
    }
    
    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');
    
    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})`)
    
    Cee Nell's avatar
    Cee Nell committed
        .text("Consensus");
    
        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
    
    Cee Nell's avatar
    Cee Nell committed
        .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)
    
    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))); });
    
    Cee Nell's avatar
    Cee Nell committed
    
          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();
      }
      
    
    Cee Nell's avatar
    Cee Nell committed
    function updateChart() {
    
    Cee Nell's avatar
    Cee Nell committed
      console.log('Update chart called');
    
    Cee Nell's avatar
    Cee Nell committed
      // Set the y scale
      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 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]);
    
    Cee Nell's avatar
    Cee Nell committed
      // 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(250, 250, 250, 0.93)');
    
      // 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(250, 250, 250, 0.93)')
        .attr('cx', d => d.x)  // Use existing x position
        .attr('cy', d => d.y)
    
    Cee Nell's avatar
    Cee Nell committed
        .on('mouseover', handleMouseOver)
        .on('mouseout', handleMouseOut)
    
    Cee Nell's avatar
    Cee Nell committed
        .merge(bubbles);
    
      // Ensure all bubbles are handled correctly
      bubbles.exit().remove();
    }
    
    Cee Nell's avatar
    Cee Nell committed
    
    //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
      }
    
    
    Cee Nell's avatar
    Cee Nell committed
      const [x, y] = d3.pointer(event, svg.node());
      const tooltip = d3.select('#tooltip');
    
      tooltip.html('')
        .append('div')
    
    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
    
      // 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' },
    
    Cee Nell's avatar
    Cee Nell committed
        { name: 'unknown', value: d.unk_direction_total, pattern: true, stroke: dimensionColors[d.dimension.replace(' ', '')], fill: `url(#pattern-stripe)` }
    
    Cee Nell's avatar
    Cee Nell committed
      ];
    
      // 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') // Adjusted path for thicker stripes
        .attr('stroke', dimensionColors[d.dimension.replace(' ', '')])
        .attr('stroke-width', 3);  // Thicker stripes
    
      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)
    
    Cee Nell's avatar
    Cee Nell committed
        .style('fill', d => d.pattern ? `url(#${patternId})` : d.fill)
    
    Cee Nell's avatar
    Cee Nell committed
        .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);
    
    Cee Nell's avatar
    Cee Nell committed
    
        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');
    
    Cee Nell's avatar
    Cee Nell committed
    }
    
    function handleMouseOut() {
      d3.select('#tooltip').style('opacity', 0);
      d3.select(this)
        .attr('stroke', null)
        .attr('stroke-width', null);
    
    Cee Nell's avatar
    Cee Nell committed
    
        // 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;
      
      #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: rgba(250, 250, 250, 0.93);
    
    Cee Nell's avatar
    Cee Nell committed
      padding: 2px;
    
    Cee Nell's avatar
    Cee Nell committed
     // border: 2px solid black;
    
    Cee Nell's avatar
    Cee Nell committed
      border-radius: 10px;
    
      pointer-events: none; /* Prevent tooltip from blocking mouse events */
    }
    
    Cee Nell's avatar
    Cee Nell committed
    .hidden {
      display: none;
    }
    
    Cee Nell's avatar
    Cee Nell committed
    #legend-container {
      display: none; /* Hide the legend initially */
    }
    #caption-container {
      margin-top: 20px;
      font-style: italic;
      margin-left: auto;
      margin-right: auto;
    }