Newer
Older
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<section id="beeswarm">
<div id="text1" class="text-container">
<p>
Everyone needs access to clean water. People may be more vulnerable to water insecurity due to
<span
:class="['highlight', 'Demographiccharacteristics', { checked: isChecked.Demographiccharacteristics }]"
@click="toggleCategory('Demographiccharacteristics')"
>
demographic characteristics
</span>,
<span
:class="['highlight', 'Health', { checked: isChecked.Health }]"
@click="toggleCategory('Health')"
>
health
</span>,
<span
:class="['highlight', 'Livingconditions', { checked: isChecked.Livingconditions }]"
@click="toggleCategory('Livingconditions')"
>
living conditions
</span>,
<span
:class="['highlight', 'Socioeconomicstatus', { checked: isChecked.Socioeconomicstatus }]"
@click="toggleCategory('Socioeconomicstatus')"
>
socioeconomic status
</span>,
<span
:class="['highlight', 'Riskperception', { checked: isChecked.Riskperception }]"
@click="toggleCategory('Riskperception')"
>
risk perception
</span>,
<span
:class="['highlight', 'Landtenure', { checked: isChecked.Landtenure }]"
@click="toggleCategory('Landtenure')"
>
land tenure
</span>, and
<span
:class="['highlight', 'Exposure', { checked: isChecked.Exposure }]"
@click="toggleCategory('Exposure')"
>
exposure to stressors
</span> (like drought or pollution).
</p>
</div>
<div id="beeswarm-chart-container">
</div>
<!-- <div id="legend-container"></div> -->
<div id="text2" class="text-container tooltip-width">
<div id="tooltip" class="tooltip">Interact with the chart to explore evidence<br>for social vulnerability determinants.</div>
<em>Many social vulnerability determinants have been studied. Some show positive <span class="legend-box positive"></span> relationships with water insecurity, some negative <span class="legend-box negative"></span>, and others unknown <span class="legend-box unknown"></span> (Drakes et al. 2024).</em>
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
</section>
</template>
<script setup>
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;
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"
};
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 keyW = 65;
const keyH = 12;
const svgDefs = d3.select('#text2')
.attr('width', 0)
.attr('height', 0)
.append('defs');
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const pattern = svgDefs.append('pattern')
.attr('id', 'pattern-stripe')
.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);
legendData.forEach(d => {
const legend = d3.select(`.${d.name.toLowerCase()}`)
.append('svg')
.attr('width', keyW + 5)
.attr('height', keyH);
legend.append('rect')
.attr('x', 4)
.attr('y', 0)
.attr('width', keyW)
.attr('height', keyH)
.style('fill', d.pattern ? d.color : d.color)
.style('stroke', d.stroke ? d.stroke : 'none')
.style('stroke-width', d.stroke ? 3 : 0);
});
// 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);
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
}
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)])
.range([10, 70]);
const yAxis = svg.append('g')
.attr("transform", "translate(100, 0)")
.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("text-anchor", "left")
.attr("font-weight", 700)
.attr("transform", `translate(${margin.left}, ${height - (margin.bottom / 2) + 10})`)
.text("Inconclusive");
// Set forces
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);
const bubbles = svg
.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)
.alpha(0.75)
.alphaDecay(0.03);
function ticked() {
bubbles
.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)));
createLegend(); // add legend to caption
}
function toggleCategory(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 tooltip = d3.select('#tooltip');
tooltip.html('')
.append('div')
.html(`<strong>${d.determinant}</strong> 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)` }
const barWidth = 200;
const barHeight = 15;
// 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');
// Highlight the circle
d3.select(this)
.attr('stroke', dimensionColors[d.dimension.replace(' ', '')])
.attr('stroke-width', 8);
.style('fill', dimensionColors[d.dimension.replace(' ', '')]);
.style('stroke', dimensionColors[d.dimension.replace(' ', '')]);
.style('stroke', dimensionColors[d.dimension.replace(' ', '')]);
tooltip.html('Interact with the chart to explore evidence for<br>social vulnerability determinants.');
d3.select(this)
.attr('stroke', null)
.attr('stroke-width', null);
}
<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 {
}
#beeswarm-chart-container svg {
max-width: 100%;
max-height: 100%;
height: auto; /* Maintain aspect ratio */
display: inline-block;
font-weight: bold;
}
.tooltip-width {
width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.tooltip {
opacity: 1;
background: $ThemeGrey;
padding: 8px;
border-radius: 5px;
pointer-events: none;
width: 100%;
text-align: center;
}
.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;
.legend-item {
display: inline-block;
margin-right: 10px;
position: relative;
padding-left: 20px; /* Adjust padding for spacing between text and rectangle */
}
.legend-item::before {
content: '';
display: inline-block;
width: 15px;
height: 10px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}