Skip to content
Snippets Groups Projects
BeeswarmChart.vue 15.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>
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 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(217, 217, 217, 0.95)');
Cee Nell's avatar
Cee Nell committed

  // 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(217, 217, 217, 0.95)')
Cee Nell's avatar
Cee Nell committed
    .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') 
Cee Nell's avatar
Cee Nell committed
    .attr('stroke', dimensionColors[d.dimension.replace(' ', '')])
    .attr('stroke-width', 3); 
Cee Nell's avatar
Cee Nell committed

  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;
$ThemeGrey: rgba(217, 217, 217, 0.95);

#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;
}

.yLabel {
    font-weight: bold;
}

.highlight {
  color: white;
  padding: 0.25px 5px;
  border-radius: 10px;
  white-space: nowrap;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.1s;

  &.Demographiccharacteristics {
    background-color: $Demographiccharacteristics;

  &.Landtenure {
    background-color: $Landtenure;

  &.Livingconditions {
    background-color: $Livingconditions;

  &.Socioeconomicstatus {
    background-color: $Socioeconomicstatus;

  &.Health {
    background-color: $Health;

  &.Riskperception {
    background-color: $Riskperception;

  &.Exposure {
    background-color: $Exposure;

  &:not(.checked) {
    color: black;
    background-color: $ThemeGrey;
  position: absolute;
  opacity: 0;
  background:  $ThemeGrey;
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;
}