diff --git a/src/components/Beeswarm.vue b/src/components/Beeswarm.vue index 7fdee9b214443f4e05fe27cfae440823ec63dc82..6355b34a9a31dead5bc38e3d9fe35f6bfba3cb47 100644 --- a/src/components/Beeswarm.vue +++ b/src/components/Beeswarm.vue @@ -1,5 +1,5 @@ <template> - <section id="beeswarm"> +<section id="beeswarm"> <div id="title" class="text-container title-text" @@ -12,49 +12,21 @@ > <p v-html="agVsMunText.paragraph1" /> </div> - <div - id="toggle-container" - class="text-container" - aria-hidden="true" - > - <div class="graph-buttons-switch"> - <input - id="id_dataSet1" - type="radio" - class="graph-buttons-switch-input" - name="dataSet1" - value="dataSet1" - v-model="selectedDataSet" - @change="onChangeDataSet" - checked - > - <label - id="dataSet1" - for="id_dataSet1" - tabindex="0" - class="graph-buttons-switch-label " - >Determinant</label> - <input - id="id_dataSet2" - type="radio" - class="graph-buttons-switch-input" - name="dataSet2" - value="dataSet2" - v-model="selectedDataSet" - @change="onChangeDataSet" - > - <label - id="dataSet2" - for="id_dataSet2" - tabindex="0" - class="graph-buttons-switch-label" - >Indicator</label> - <span class="graph-buttons-switch-selection" /> - </div> - </div> - <div id="beeswarm-chart-container"> - + <div id="toggle-container" > + <div id="checkbox1-container"> + <input type="checkbox" id="checkbox1" v-model="checkboxStates[0]" @change="handleCheckboxChange(0)"> + <label for="checkbox1">Evidence</label> + </div> + <div id="checkbox2-container"> + <input type="checkbox" id="checkbox2" v-model="checkboxStates[1]" @change="handleCheckboxChange(1)"> + <label for="checkbox2">Agreement</label> + </div> + <div id="checkbox3-container"> + <input type="checkbox" id="checkbox3" v-model="checkboxStates[2]" @change="handleCheckboxChange(2)"> + <label for="checkbox3">Direction</label> + </div> </div> + <div id="beeswarm-chart-container"></div> <div id="title2" class="text-container title-text" @@ -97,32 +69,35 @@ const dataSet1 = ref([]); const dataSet2 = ref([]); const selectedDataSet = ref('dataSet1'); + const checkboxStates = ref([false, false, false]); + const checkboxFunctions = [sizeByEvidence, positionByAgreement, sortByDirection]; + const categoryCenters = {} let data = dataSet1; let simulation; + + // set up svg let svg; - const height = 600; const width = 1000; let margin = {top: 100, right: 20, bottom: 20, left: 40} // set colors for bubble charts const dimensionColors = { - Demographiccharacteristics: "#625D0B", - Landtenure: "#5C0601", - Livingconditions: "#0B4E8B", - Socioeconomicstatus: "#DC8260", - Health: "#7F4A89", - Riskperception: "#249CB1", - Exposure: "#B47D83" - } + Demographiccharacteristics: "#625D0B", + Landtenure: "#5C0601", + Livingconditions: "#0B4E8B", + Socioeconomicstatus: "#DC8260", + Health: "#7F4A89", + Riskperception: "#249CB1", + Exposure: "#B47D83" + } // load data and then make chart onMounted(() => { console.log("component mounted"); loadDatasets().then(() => { if (selectedDataSet.value.length > 0) { - addToggle() - createBubbleChart(selectedDataSet); + createBeeswarmChart(selectedDataSet); } else { console.error('Error loading data:', error) } @@ -145,41 +120,6 @@ }); }; - function addToggle() { - const toggleContainer = document.getElementById('toggle-container'); - const toggleLabels = toggleContainer.querySelectorAll('.graph-buttons-switch-label'); - const selectionIndicator = toggleContainer.querySelector('.graph-buttons-switch-selection'); - - toggleLabels.forEach(label => { - label.addEventListener('click', () => { - // Remove active class from all labels - toggleLabels.forEach(l => l.classList.remove('active')); - // Add active class to the clicked label - label.classList.add('active'); - - // Update selection indicator position - const rect = label.getBoundingClientRect(); - selectionIndicator.style.left = `${rect.left - toggleContainer.getBoundingClientRect().left}px`; - - // Update selectedDataSet value based on the clicked label - selectedDataSet.value = label.getAttribute('for'); - // Run functions based on selected dataset - if (label.getAttribute('for') === 'id_dataSet1') { - resetBubbles(dataSet1); - } else if (label.getAttribute('for') === 'id_dataSet2') { - categorizeBubbles(dataSet2); - } - }); - }); - - // Set initial position of selection indicator - const defaultLabel = toggleContainer.querySelector('.graph-buttons-switch-label.active'); - if (defaultLabel) { - const rect = defaultLabel.getBoundingClientRect(); - selectionIndicator.style.left = `${rect.left - toggleContainer.getBoundingClientRect().left}px`; - } - } - // helper function to set up mouse events function createMouseEvents() { const tooltip = d3.selectAll('#beeswarm-chart-container') @@ -219,6 +159,13 @@ return {handleMouseOver, handleMouseMove, handleMouseOut} } + function handleCheckboxChange(index) { + // console.log(checkboxStates); + // console.log(checkboxStates.value[index]); + // Call the corresponding function based on the index + checkboxFunctions[index](checkboxStates.value[index]); + } + // call helper function const { handleMouseOver, handleMouseMove, handleMouseOut } = createMouseEvents(); @@ -236,189 +183,243 @@ return { xScale, radiusScale} } - // initiate chart with determinant indicator dataset - function createBubbleChart() { - svg = d3.selectAll('#beeswarm-chart-container') - .append('svg') // TO DO if I can figure out how to take this out of createBubbleChart we can reuse this instead of resetBubbles repeating code. - .attr('class', 'beeswarmSvg') - .attr('width', width) - .attr('height', height); + function createBeeswarmChart() { + svg = d3 + .selectAll('#beeswarm-chart-container') + .append('svg') + .attr('class', 'beeswarmSvg') + .attr('width', width) + .attr('height', height) - const { xScale, radiusScale} = createScales(data); + const radius = 10 - // add xAxis g to svg - const xAxis = svg.append('g') - .attr("transform", "translate(0," + (height - 50) + ")") - .call(d3.axisBottom(xScale)) + const categories = Array.from(new Set(data.value.map((d) => d.dimension))) + // console.log(categories) + // const categoryCenters = {} - // add label to x axis - svg.append('text') - .attr("class", "xLabel") - .attr("text-anchor", "middle") - .attr("x", width/2) - .attr("y", height-10) - .text("Level of Agreement"); + const numRows = 2 //Math.ceil(Math.sqrt(categories.length)); // Number of rows for grid layout + const numCols = 4 //Math.ceil(categories.length / numRows); // Number of columns + const colWidth = width / numCols + const rowHeight = height / numRows - // define forces that control the bubbles - const forceX = d3.forceX(d => xScale(d.level_agreement)).strength(1); - const forceY = d3.forceY((height/1.5) - (margin.bottom/2))//.strength(0.1); - const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 2); //forceCollide for bubbles sized by evidence_val - const forceManyBody = d3.forceManyBody().strength(-15); // Adjust the strength as needed - - const bubbles = svg.selectAll(".bubble") - .data(data.value) - .enter().append("circle") - .attr("class", d => "bubble " + d.dimension.replace(" ", "")) - .attr('r', d => radiusScale(d.evidence_val)) // size bubbles based on evidence_val - .style('fill', d => dimensionColors[d.dimension.replace(" ", "")]); - - // run force simulation - simulation = d3.forceSimulation() - .force("x", forceX) - .force("y", forceY) - .force("collide", forceCollide) - .force("charge", forceManyBody) - .nodes(data.value) - .on("tick", ticked) - .alpha(.2) - .restart(); + categories.forEach((category, index) => { + let col = index % numCols // Column index + let row = Math.floor(index / numCols) // Row index - bubbles - .on("mouseover", handleMouseOver) - .on("mousemove", handleMouseMove) - .on("mouseout", handleMouseOut); + // Offset columns in the second row + if (row === 1) { + col += 0.5 // Offset by half a column width + } - function ticked() { - bubbles - .attr("cx", d => d.x) - .attr("cy", d => d.y); - } + // Calculate the center position + categoryCenters[category] = { + x: colWidth * col + colWidth / 2, + y: rowHeight * row + rowHeight / 2 + } + }) + + data.value.forEach((d) => { + // Add randomness to the initial positions + const center = categoryCenters[d.dimension] + d.x = center.x + (Math.random() - 0.5) * 50 // Adjust randomness factor (50 here) as needed + d.y = center.y + (Math.random() - 0.5) * 50 + }) + + // define forces that control the bubbles + const forceX = d3.forceX((d) => categoryCenters[d.dimension].x).strength(0.2) + const forceY = d3.forceY((d) => categoryCenters[d.dimension].y).strength(0.1) + const forceCollide = d3.forceCollide(radius + 2) + const forceManyBody = d3.forceManyBody().strength(-15) // Adjust the strength as needed + + // add bubbles to the page + const bubbles = svg + .selectAll('.bubble') + .data(data.value) + .enter() + .append('circle') + .attr('class', 'bubble') + .attr('r', radius) + .style('fill', (d) => dimensionColors[d.dimension.replace(' ', '')]) + + const simulation = d3 + .forceSimulation() + .force('x', forceX) + .force('y', forceY) + .force('collide', forceCollide) + .force('charge', forceManyBody) + .nodes(data.value) + .on('tick', ticked) + .alpha(0.2) + .restart() + + bubbles + .on('mouseover', handleMouseOver) + .on('mousemove', handleMouseMove) + .on('mouseout', handleMouseOut) + + function ticked() { + bubbles.attr('cx', (d) => d.x).attr('cy', (d) => d.y) + } } - // takes indicator_uncertainty.csv and categorizes bubbles by agreement direction - function categorizeBubbles(data) { - //remove determinant uncertainty bubbles - svg.selectAll(".bubble").remove(); - - const { xScale, radiusScale} = createScales(data); - - //separate beeswarms along the y axis based on agreement direction - var yScale = d3.scaleBand() - .domain(data.value.map(function(d) { return d.sig_name; })) - .range([margin.top, height]) - - // Add new bubbles with updated data - const bubbles = svg.selectAll(".bubble") - .data(data.value) - .enter().append("circle") - .attr("class", d => "bubble " + (d.dimension.replace(" ", "")) + " " + (d.indicator.replace(" ", "")) + " " + (d.sig_name)) - .attr('r', d => radiusScale(d.sig_value)) - .style('fill', d => dimensionColors[d.dimension.replace(" ", "")]); - - // define forces that control the bubbles - const forceX = d3.forceX(d => xScale(d.level_agreement)).strength(.4); - const forceY = d3.forceY(d => yScale(d.sig_name)).strength(.4) - const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 1); //forceCollide for bubbles sized by evidence_val - const forceManyBody = d3.forceManyBody().strength(-15); // Adjust the strength as needed - - // run force simulation - simulation = d3.forceSimulation() - .force("x", forceX) - .force("y", forceY) - .force("collide", forceCollide) - .force("charge", forceManyBody) - .nodes(data.value) - .on("tick", ticked) - .alpha(.2) - .restart(); - - bubbles - .on("mouseover", handleMouseOver) - .on("mousemove", handleMouseMove) - .on("mouseout", handleMouseOut); + function sizeByEvidence(checked) { + if (checked){ + const { radiusScale } = createScales(data) + + const forceCollide = d3.forceCollide((d) => radiusScale(d.evidence_val) + 2) + const forceManyBody = d3.forceManyBody().strength(15) // Adjust the strength as needed + const bubbles = svg.selectAll('.bubble') + .attr('r', d => radiusScale(d.evidence_val)) // size bubbles based on evidence_val + + const simulation = d3 + .forceSimulation() + // .force('x', forceX) + // .force('y', forceY) + .force('collide', forceCollide) + .force('charge', forceManyBody) + .nodes(data.value) + .on('tick', ticked) + .alpha(0.2) + .restart() + function ticked() { - bubbles - .attr("cx", d => d.x) - .attr("cy", d => d.y); + bubbles.attr('cx', (d) => d.x).attr('cy', (d) => d.y) } + } else { + const radius = 10 - // labeling categories - // const uniqueSigNames = [...new Set(data.value.map(d => d.sig_name.replace(/_/g, ' ')))]; - const uniqueSigNames = ["Positively Related", "Negatively Related", "Unknown Direction"] - const numCategories = uniqueSigNames.length; - const swarmHeight = height / numCategories; - - const categoryLabels = svg.selectAll(".category-label") - .data(uniqueSigNames) - .enter() - .append("text") - .attr("class", "category-label") - .attr("x", margin.right+100) // Position labels in the center of each swarm - .attr("y", (d, i) => swarmHeight * (i + .2)) // Adjust as needed to position above the swarms - .attr("text-anchor", "middle") - .text(d => d) - .style("opacity", 1); + const forceX = d3.forceX((d) => categoryCenters[d.dimension].x).strength(0.2) + const forceY = d3.forceY((d) => categoryCenters[d.dimension].y).strength(0.1) + const forceCollide = d3.forceCollide(radius + 2) + const forceManyBody = d3.forceManyBody().strength(-15) // Adjust the strength as needed + + const bubbles = svg.selectAll('.bubble') + .attr('r', radius) + + const simulation = d3 + .forceSimulation() + .force('x', forceX) + .force('y', forceY) + .force('collide', forceCollide) + .force('charge', forceManyBody) + .nodes(data.value) + .on('tick', ticked) + .alpha(0.2) + .restart() + + function ticked() { + bubbles.attr('cx', (d) => d.x).attr('cy', (d) => d.y) + } + } } - // resets the bubbles using determinant_indicators.csv - function resetBubbles(data) { - // remove indicator uncertainty bubbles and category labels - svg.selectAll(".bubble").remove(); - svg.selectAll(".category-label").remove(); - + function positionByAgreement(checked) { + if (checked) { const { xScale, radiusScale} = createScales(data); - // Add new bubbles with updated data - const bubbles = svg.selectAll(".bubble") - .data(data.value) - .enter().append("circle") - .attr("class", d => "bubble " + d.dimension.replace(" ", "")) - .attr('r', d => radiusScale(d.evidence_val)) - .style('fill', d => dimensionColors[d.dimension.replace(" ", "")]); + const xAxis = svg.append('g') + .attr("transform", "translate(0," + (height - 50) + ")") + .call(d3.axisBottom(xScale)) - // define forces that control the bubbles + // add label to x axis + svg.append('text') + .attr("class", "xLabel") + .attr("text-anchor", "middle") + .attr("x", width/2) + .attr("y", height-10) + .text("Level of Agreement"); + const forceX = d3.forceX(d => xScale(d.level_agreement)).strength(1); const forceY = d3.forceY((height/1.5) - (margin.bottom/2))//.strength(0.1); const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 2); //forceCollide for bubbles sized by evidence_val const forceManyBody = d3.forceManyBody().strength(-15); // Adjust the strength as needed + + const bubbles = svg.selectAll(".bubble") - // run force simulation simulation = d3.forceSimulation() - .force("x", forceX) - .force("y", forceY) - .force("collide", forceCollide) - .force("charge", forceManyBody) - .nodes(data.value) - .on("tick", ticked) - .alpha(.2) - .restart(); - - bubbles - .on("mouseover", handleMouseOver) - .on("mousemove", handleMouseMove) - .on("mouseout", handleMouseOut); + .force("x", forceX) + .force("y", forceY) + .force("collide", forceCollide) + .force("charge", forceManyBody) + .nodes(data.value) + .on("tick", ticked) + .alpha(.2) + .restart(); function ticked() { - bubbles - .attr("cx", d => d.x) - .attr("cy", d => d.y); + bubbles + .attr("cx", d => d.x) + .attr("cy", d => d.y); } + } else { - // hide agreement category labels - const categoryLabels = svg.selectAll(".category-label") - .style("opacity", 0); + } + } + function sortByDirection() { + const { xScale, radiusScale} = createScales(data); + + var yScale = d3.scaleBand() + .domain(data.value.map(function(d) { return d.sig_name; })) + .range([margin.top, height]) + + const bubbles = svg.selectAll(".bubble") + + // const forceX = d3.forceX(d => xScale(d.level_agreement)).strength(.4); + const forceY = d3.forceY(d => yScale(d.sig_name)).strength(.4) + const forceCollide = d3.forceCollide(d => radiusScale(d.evidence_val) + 1); //forceCollide for bubbles sized by evidence_val + const forceManyBody = d3.forceManyBody().strength(-15); // Adjust the strength as needed + + simulation = d3.forceSimulation() + // .force("x", forceX) + .force("y", forceY) + .force("collide", forceCollide) + .force("charge", forceManyBody) + .nodes(data.value) + .on("tick", ticked) + .alpha(.2) + .restart(); + + function ticked() { + bubbles + .attr("cx", d => d.x) + .attr("cy", d => d.y); + } } </script> <style scoped lang="scss"> $switchWidth: 12rem; - #toggle-container { display: flex; + justify-content: center; + align-items: center; width: 100%; + margin: 20px 0; /* Add some margin to give space around the toggle container */ +} + +#checkbox1-container, +#checkbox2-container, +#checkbox3-container { + display: flex; + align-items: center; + margin: 0 10px; /* Add some space between the checkboxes */ +} + +#checkbox1-container label, +#checkbox2-container label, +#checkbox3-container label { + margin-left: 5px; /* Add space between the checkbox and the label */ + font-size: 2rem; /* Increase label font size if needed */ +} + +#checkbox1, +#checkbox2, +#checkbox3 { + transform: scale(1.5); /* Adjust the scale to make checkboxes bigger */ + margin-right: 10px; /* Add some space between checkbox and label */ } .graph-buttons-switch { @@ -432,18 +433,19 @@ $switchWidth: 12rem; -webkit-box-shadow: inset 0 0.1rem 0.3rem rgba(0, 0, 0, 0.1), 0 0.1remx rgba(255, 255, 255, 0.1); box-shadow: inset 0 0.1rem 0.3rem rgba(0, 0, 0, 0.1), 0 0.1rem rgba(255, 255, 255, 0.1); - -webkit-touch-callout: none; /* iOS Safari */ - -webkit-user-select: none; /* Safari */ - -khtml-user-select: none; /* Konqueror HTML */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* Internet Explorer/Edge */ - user-select: none; /* Non-prefixed version, currently - supported by Chrome and Opera */ + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ + @media screen and (max-width: 600px) { - height: 2.6rem; + height: 2.6rem; } - } - .graph-buttons-switch-label { +} + +.graph-buttons-switch-label { position: relative; z-index: 2; float: left; @@ -451,30 +453,40 @@ $switchWidth: 12rem; line-height: 2.4rem; text-align: center; cursor: pointer; + @media screen and (max-width: 600px) { - line-height: 2.2rem; - width: $switchWidth * 1.02; + line-height: 2.2rem; + width: $switchWidth * 1.02; } - } - .graph-buttons-switch-label-off { +} + +.graph-buttons-switch-label-off { padding-left: 0.2rem; padding-right: 0.2rem; - } - .graph-buttons-switch-label-on { +} + +.graph-buttons-switch-label-on { padding-left: 0.2rem; padding-right: 0.2rem; - } - .graph-buttons-switch-input {display: none;} - .graph-buttons-switch-input:checked + .graph-buttons-switch-label { +} + +.graph-buttons-switch-input { + display: none; +} + +.graph-buttons-switch-input:checked + .graph-buttons-switch-label { font-weight: bold; -webkit-transition: 0.3s ease-out; -moz-transition: 0.3s ease-out; -o-transition: 0.3s ease-out; transition: 0.3s ease-out; - } - .graph-buttons-switch-input:checked + .graph-buttons-switch-label-on ~ .graph-buttons-switch-selection {left: $switchWidth;} +} - .graph-buttons-switch-selection { +.graph-buttons-switch-input:checked + .graph-buttons-switch-label-on ~ .graph-buttons-switch-selection { + left: $switchWidth; +} + +.graph-buttons-switch-selection { display: block; position: absolute; z-index: 1; @@ -482,19 +494,19 @@ $switchWidth: 12rem; left: 0.2rem; width: $switchWidth; height: 2.4rem; - background: rgba(255, 255, 255,1); + background: rgba(255, 255, 255, 1); border-radius: 0.2rem; - -webkit-box-shadow: inset 0 0.1rem rgba(255, 255, 255,0.6), 0 0 0.2rem rgba(0, 0, 0, 0.3); - box-shadow: inset 0 0.1rem rgba(255, 255, 255,0.6), 0 0 0.2rem rgba(0, 0, 0, 0.3); - -webkit-transition: left 0.3s ease-out,background 0.3s; - -moz-transition: left 0.3s ease-out,background 0.3s; - -o-transition: left 0.3s ease-out,background 0.3s; - transition: left 0.3s ease-out,background 0.3s ; - /* transition: background 0.3s ; */ + -webkit-box-shadow: inset 0 0.1rem rgba(255, 255, 255, 0.6), 0 0 0.2rem rgba(0, 0, 0, 0.3); + box-shadow: inset 0 0.1rem rgba(255, 255, 255, 0.6), 0 0 0.2rem rgba(0, 0, 0, 0.3); + -webkit-transition: left 0.3s ease-out, background 0.3s; + -moz-transition: left 0.3s ease-out, background 0.3s; + -o-transition: left 0.3s ease-out, background 0.3s; + transition: left 0.3s ease-out, background 0.3s; + @media screen and (max-width: 600px) { - height: 2.2rem; + height: 2.2rem; } - } +} #beeswarm-chart-container { text-align: center; @@ -518,16 +530,6 @@ $switchWidth: 12rem; user-select: none; } -// #button-container { -// display: flex; -// justify-content: center; -// margin-top: 20px; -// } - -// #button-container button { -// margin: 0 10px; -// } - .tooltip { font-size: 16px; background-color: white; @@ -536,14 +538,10 @@ $switchWidth: 12rem; border-radius: 5px; padding: 5px; position: absolute; - // pointer-events: none; } -// TO DO: not working text.xLabel { font-weight: 600; } - </style> - \ No newline at end of file