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>
<caption>
A meta-analysis by Drakes et al. (2024) evaluated how water insecurity in the Western U.S. is influenced by social determinants. Interact with the chart and the highlighted text above to see what they found.
</caption>
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"
};
// 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})`)
.text("Level of Agreement");
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();
}
function 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(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)
.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
}
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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: 'white' }
];
// 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)
.style('fill', d => d.pattern ? 'url(#pattern-stripe)' : 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);
}
function handleMouseOut() {
d3.select('#tooltip').style('opacity', 0);
d3.select(this)
.attr('stroke', null)
.attr('stroke-width', null);
}
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
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
</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;
pointer-events: none; /* Prevent tooltip from blocking mouse events */
}