Newer
Older
Everyone needs access to clean water. People may be more vulnerable to water insecurity due to
:class="['highlight', 'Demographiccharacteristics', { checked: isChecked.Demographiccharacteristics }]"
@click="toggleCategory('Demographiccharacteristics')"
:class="['highlight', 'Health', { checked: isChecked.Health }]"
@click="toggleCategory('Health')"
:class="['highlight', 'Livingconditions', { checked: isChecked.Livingconditions }]"
@click="toggleCategory('Livingconditions')"
:class="['highlight', 'Socioeconomicstatus', { checked: isChecked.Socioeconomicstatus }]"
@click="toggleCategory('Socioeconomicstatus')"
:class="['highlight', 'Riskperception', { checked: isChecked.Riskperception }]"
@click="toggleCategory('Riskperception')"
:class="['highlight', 'Landtenure', { checked: isChecked.Landtenure }]"
@click="toggleCategory('Landtenure')"
:class="['highlight', 'Exposure', { checked: isChecked.Exposure }]"
@click="toggleCategory('Exposure')"
>
exposure to stressors
</span> (like drought or pollution).
</p>
</div>
<div id="tooltip" style="position: absolute; opacity: 0;"></div>
<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>
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 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"
};
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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');
} catch (error) {
console.error('Error during component mounting', error);
});
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);
}
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 [];
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]);
// 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)])
.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")
.attr("transform", `translate(${margin.left}, ${margin.top/2})`)
svg.append('text')
.attr("class", "yLabel")
.attr("transform", `translate(${margin.left}, ${height - (margin.bottom/2) + 10})`)
const forceY = d3.forceY(d => yScale(d.level_agreement)).strength(0.7);
const forceX = d3.forceX(margin.left + (width / 2)).strength(0.2);
const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 2).iterations(20);
const forceManyBody = d3.forceManyBody().strength(1);
.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)
.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();
}
// Set the y scale
const yScale = d3.scaleLinear()
.domain([40, d3.max(data.value, d => d.level_agreement)])
.range([height - margin.bottom, margin.top]);
// 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]);
// 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)');
// 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)')
.attr('cx', d => d.x) // Use existing x position
.attr('cy', d => d.y)
.on('mouseover', handleMouseOver)
.on('mouseout', handleMouseOut)
.merge(bubbles);
// Ensure all bubbles are handled correctly
bubbles.exit().remove();
}
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
}
const [x, y] = d3.pointer(event, svg.node());
const tooltip = d3.select('#tooltip');
tooltip.html('')
.append('div')
.html(`<strong>${d.determinant}</strong><br>appeared in ${d.evidence_val} ${d.evidence_val === 1 ? 'study' : 'studies'}.`);
// 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' },
{ name: 'unknown', value: d.unk_direction_total, pattern: true, stroke: dimensionColors[d.dimension.replace(' ', '')], fill: `url(#pattern-stripe)` }
];
// 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')
.attr('stroke', dimensionColors[d.dimension.replace(' ', '')])
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)
.style('fill', d => d.pattern ? `url(#${patternId})` : d.fill)
.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);
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');
}
function handleMouseOut() {
d3.select('#tooltip').style('opacity', 0);
d3.select(this)
.attr('stroke', null)
.attr('stroke-width', null);
// Hide the legend
//d3.select('#legend-container').style('display', 'none');
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
<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;
pointer-events: none; /* Prevent tooltip from blocking mouse events */
}
#legend-container {
display: none; /* Hide the legend initially */
}
#caption-container {
margin-top: 20px;
font-style: italic;
margin-left: auto;
margin-right: auto;
}