Newer
Older
<section id="beeswarm">
<div id="text1" class="text-container">
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
<p v-html="bubbleCheckboxText"></p>
<!-- Render the translated labels within the span elements -->
<p>
<span
:class="['highlight', 'Demographiccharacteristics', { checked: isChecked.Demographiccharacteristics }]"
@click="toggleCategory('Demographiccharacteristics')"
>
{{ demographicText }}
</span>,
<span
:class="['highlight', 'Health', { checked: isChecked.Health }]"
@click="toggleCategory('Health')"
>
{{ healthText }}
</span>,
<span
:class="['highlight', 'Livingconditions', { checked: isChecked.Livingconditions }]"
@click="toggleCategory('Livingconditions')"
>
{{ livingConditionsText }}
</span>,
<span
:class="['highlight', 'Socioeconomicstatus', { checked: isChecked.Socioeconomicstatus }]"
@click="toggleCategory('Socioeconomicstatus')"
>
{{ socioeconomicStatusText }}
</span>,
<span
:class="['highlight', 'Riskperception', { checked: isChecked.Riskperception }]"
@click="toggleCategory('Riskperception')"
>
{{ riskPerceptionText }}
</span>,
<span
:class="['highlight', 'Landtenure', { checked: isChecked.Landtenure }]"
@click="toggleCategory('Landtenure')"
>
{{ landTenureText }}
</span>, &
<span
:class="['highlight', 'Exposure', { checked: isChecked.Exposure }]"
@click="toggleCategory('Exposure')"
>
{{ exposureText }}
</span>
<span v-html="bubbleCheckboxEndText" />
</p>
<div id="text2" class="text-container tooltip-width">
<div id="tooltip" class="tooltip">
<p v-html="bubbleTooltipText"></p>
</div>
<em><p v-html="bubbleLegendText"></p></em>
<div id="beeswarm-chart-container"></div>
<div id="text2" class="text-container tooltip-width">
<em><p v-html="bubbleYAxisText"></p></em>
import { ref, computed, onMounted, watch } from "vue";
const { t, locale } = useI18n();
const userLang = navigator.language || navigator.userLanguage;
const isSpanish = userLang.startsWith('es');
// Global variables
const publicPath = import.meta.env.BASE_URL;
const data = ref([]);
// Set up SVG
let svg;
const height = 600;
const margin = { top: 40, right: 20, bottom: 40, left: 40 };
// Watch for changes in the locale and update the chart labels when the language is toggled
watch(locale, () => {
updateYAxisLabels();
});
// Function to update Y-axis labels when language changes
function updateYAxisLabels() {
if (svg) {
// Update y-axis max label
svg.select('.yLabelMax')
.text(bubbleYlabelMax.value);
// Update y-axis min label
svg.select('.yLabelMin')
.text(bubbleYlabelMin.value);
}
}
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: "#092734",
Landtenure: "#0C5048",
Livingconditions: "#45A196",
Socioeconomicstatus: "#FF5F33",
Exposure: "#CC8500" //C08A16
};
// Computed properties for translations
const bubbleCheckboxText = computed(() => t('text.components.chartText.bubbleCheckbox'));
const bubbleCheckboxEndText = computed(() => t('text.components.chartText.bubbleCheckboxEnd'));
const bubbleTooltipText = computed(() => t('text.components.chartText.bubbleText'));
const bubbleLegendText = computed(() => t('text.components.chartText.bubbleLegend'));
const bubbleYAxisText = computed(() => t('text.components.chartText.bubbleYaxis'));
const bubbleYlabelMax = computed(() => t('text.components.chartText.bubbleYlabelMax'));
const bubbleYlabelMin = computed(() => t('text.components.chartText.bubbleYlabelMin'));
// Labels for each category
const demographicText = computed(() => t('text.components.chartText.bubbleLabels.Demographiccharacteristics'));
const healthText = computed(() => t('text.components.chartText.bubbleLabels.Health'));
const livingConditionsText = computed(() => t('text.components.chartText.bubbleLabels.Livingconditions'));
const socioeconomicStatusText = computed(() => t('text.components.chartText.bubbleLabels.Socioeconomicstatus'));
const riskPerceptionText = computed(() => t('text.components.chartText.bubbleLabels.Riskperception'));
const landTenureText = computed(() => t('text.components.chartText.bubbleLabels.Landtenure'));
const exposureText = computed(() => t('text.components.chartText.bubbleLabels.Exposure'));
// Computed function for generating tooltip text
const generateTooltipText = (dataPoint) => {
return computed(() => {
const translatedDeterminant = locale.value === 'es' ? dataPoint.determinant_es : dataPoint.determinant_wrapped;
const determinant = `<strong>${translatedDeterminant}</strong>`;
const count = dataPoint.evidence_val;
const studyLabel = dataPoint.evidence_val === 1 ? t('text.components.chartText.singleStudy') : t('text.components.chartText.multipleStudies');
const appeared = t('text.components.chartText.appeared');
return `${determinant} ${appeared} ${count} ${studyLabel}`;
});
};
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'] }
];
// create legend for stack bar charts
function createPattern() {
const svgDefs = d3.select('body')
.attr('width', 0)
.attr('height', 0)
.append('defs');
.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')
// Load data and then make chart
onMounted(async () => {
try {
data.value = await loadJsonData('determinant_uncertainty.json');
if (data.value.length > 0) {
createBeeswarmChart();
} else {
console.error('Error loading data');
} catch (error) {
console.error('Error during component mounting', error);
async function loadJsonData(fileName) {
const loadedData = await d3.json(publicPath + fileName);
return loadedData;
} catch (error) {
console.error(`Error loading data from ${fileName}`, error);
return [];
}
}
function createBeeswarmChart() {
// Remove any existing SVG element
d3.select('#beeswarm-chart-container').select('svg').remove();
// Get dynamic dimensions to draw chart
const containerWidth = document.getElementById('beeswarm-chart-container').offsetWidth;
const containerHeight = window.innerWidth <= 700 ? window.innerHeight * 0.70 : 650;
const margin = window.innerWidth <= 700 ? { top: 40, right: 10, bottom: 40, left: 10 } : { top: 40, right: 20, bottom: 40, left: 40 };
const width = containerWidth - margin.left - margin.right;
svg = d3
.select('#beeswarm-chart-container')
.append('svg')
.attr('class', 'beeswarmSvg')
.attr('viewBox', `0 0 ${containerWidth} ${containerHeight}`)
.attr('preserveAspectRatio', 'xMidYMid meet')
const yScale = d3.scaleLinear()
.domain([40, d3.max(data.value, d => d.level_agreement)])
// 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(window.innerWidth <= 700 ? [8, 40] : [10, 70]);
.call(d3.axisLeft(yScale)
.ticks(5)
.tickFormat(d => d + '%'))
.attr("stroke-width", 2)
.attr("class", "yTicks")
.attr("font-size", "18px");
// Add label to y axis
svg.append('text')
.attr("transform", `translate(${margin.left}, ${margin.top / 2})`)
.attr("text-anchor", "left")
.attr("font-weight", 700)
.attr("transform", `translate(${margin.left}, ${containerHeight - (margin.bottom / 2) + 10})`)
const forceY = d3.forceY(d => yScale(d.level_agreement)).strength(0.9);
const forceX = d3.forceX(margin.left + (width / 2)).strength(0.1);
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);
.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(containerHeight - radiusScale(d.evidence_val), d.y)));
}
function toggleCategory(category) {
isChecked.value[category] = !isChecked.value[category];
updateChart();
}
// Adjust height based on screen size
const isMobile = window.innerWidth <= 700;
const containerHeight = isMobile ? window.innerHeight * 0.50 : height;
.domain([40, d3.max(data.value, d => d.level_agreement)])
// 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(window.innerWidth <= 700 ? [8, 40] : [10, 70]);
// Filter data based on active categories
const activeCategories = Object.keys(isChecked.value).filter(category => isChecked.value[category]);
// 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();
}
function updatePatternColor(color) {
d3.select(`#${patternId} path`)
.attr('stroke', color);
}
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
}
/// Select the tooltip element
// Generate the tooltip text using the computed property
const tooltipText = generateTooltipText(d).value;
// Update the tooltip with the constructed text
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 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');
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)
updatePatternColor(dimensionColors[d.dimension.replace(' ', '')]);
// add emphasis to bubbles
d3.select(this)
.attr('stroke', dimensionColors[d.dimension.replace(' ', '')])
.attr('stroke-width', 8);
// update colors in legend
d3.select('#text2').select('.positive').select('rect')
.style('fill', dimensionColors[d.dimension.replace(' ', '')]);
d3.select('#text2').select('.negative').select('rect')
.style('fill', 'white')
.style('stroke', dimensionColors[d.dimension.replace(' ', '')]);
d3.select('#text2').select('.unknown').select('rect')
.style('fill', `url(#pattern-stripe)`)
.style('stroke', dimensionColors[d.dimension.replace(' ', '')]);
d3.select('#text2').select(`#${patternId} path`)
.attr('stroke', dimensionColors[d.dimension.replace(' ', '')]);
const tooltipText = t('text.components.chartText.bubbleText');
d3.select(this)
.attr('stroke', null)
.attr('stroke-width', null);
}
$Demographiccharacteristics: var(--color-Demographiccharacteristics);
$Landtenure: var(--color-Landtenure);
$Livingconditions: var(--color-Livingconditions);
$Socioeconomicstatus: var(--color-Socioeconomicstatus);
$Health: var(--color-Health);
$Riskperception: var(--color-Riskperception);
$Exposure: var(--color-Exposure);
$ThemeGrey: var(--color-themegrey);
}
#beeswarm-chart-container svg {
max-height: 100%;
height: auto; /* Maintain aspect ratio */
.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%;
}
.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;
font-size: 16px;
}
.yTicks {
font-size: 12px;
}
#beeswarm-chart-container {
width: 100vw; /* Make the chart container take up 100% of the viewport width on mobile */
height: auto; /* Make the chart container take up the full height of the screen */
}
#beeswarm-chart-container svg {
height: 100%; /* Make the SVG fill the container height */
}
#mobile-tooltip {
display: block; /* Show the mobile tooltip only on mobile devices */
margin: 10px;
}
}
#water-insecurity-tooltip {
margin-left: -160px;
}