Skip to content
Snippets Groups Projects
BeeswarmChart.vue 11.9 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')"
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">
        <div id="tooltip" style="position: absolute; opacity: 0; background: #f9f9f9; padding: 5px; border: 1px solid #ccc; border-radius: 5px;"></div>
      </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([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)
      .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);
Cee Nell's avatar
Cee Nell committed
    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))
Cee Nell's avatar
Cee Nell committed
      .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`)
Cee Nell's avatar
Cee Nell committed
        .style('left', (x + 10) + 'px')
        .style('top', (y - 28) + 'px');

        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)))
          .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))
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;
  }
  #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 */
}