I'm trying to make my own beeswarm plot, which is a plot of points across one dimension where the points move up as needed in order to not overlap with other points.
Mine has labels for each point, and I'm trying to work out the javascript to move them up from the baseline only if they collide with a previous point. However, they're moving up regardless of if they collide or not.
Desired behavior: In the first example (depending on your screen width) ● Juliet Francis
should be down at the baseline instead of way up near the top, because it can exist at the baseline without colliding with any preceding points (ordered from right to left).
I've tested the collision function, and that seems to be working fine. The problem seems to be either in swarm()
or collidesAll()
functions.
I've been poking at this for a couple days and haven't been able to get it working. A second pair of eyes would be appreciated.
$(function() {
let $graphs = $('.graph')
let $firstSetOfNames = $('.graph:first-child .graph-dd')
const setup = () => {
let longest = 0
$firstSetOfNames.each(function() {
longest = ($(this).width() > longest) ? $(this).width() : longest
})
$graphs.css('padding-right', longest + 'px')
}
const collides = ($e1, $e2) => {
let e1x1 = $e1.offset().left
let e1x2 = e1x1.x1 + $e1.outerWidth( true )
let e2x1 = $e2.offset().left
let e2x2 = e2x1.x1 + $e2.outerWidth( true )
let x = ((e1x1 < e2x1) && (e2x1 < e1x2)) || ((e2x1 < e1x1) && (e1x1 < e2x2))
let y = parseInt($e1.css('--y'), 10) === parseInt($e2.css('--y'), 10)
return !!(x || y)
}
const collidesAll = ($people, $person, j) => {
for (let i = 0; i < j; i++) {
if (collides($person, $people.eq(i))) {
return true
}
}
return false
}
const swarm = () => {
$graphs.each(function(i) {
let $graph = $(this)
let $people = $($graph.find('.graph-dd').get().reverse())
$people.each(function(j) {
let $person = $(this)
let n = 1
if (0 === j) {
$person.css('--y', 1)
} else {
do {
$person.css('--y', n++)
} while (collidesAll($people, $person, j))
$graph.css('--yMax', n)
}
})
})
}
setup()
swarm()
})
.graph {
margin: 2rem;
padding: calc(calc(var(--yMax, 0) * 1.1em) + 1rem) 1rem 1rem;
border: 1px solid black;
overflow: hidden;
}
.graph-dl {
position: relative;
display: flex;
margin: 0;
justify-content: space-between;
}
.graph-dl::before {
content: "";
position: absolute;
display: block;
height: 1px;
top: calc(50% - 0.5px);
left: 0;
right: 0;
background: gray;
z-index: -1;
}
.graph-dt {
background: black;
width: 1px;
height: 1em;
border: 0.5em solid white;
margin: 0 -0.5em;
}
.graph-dt span {
display: none;
}
.graph-dd {
position: absolute;
display: block;
top: 0.5em;
margin: 0 0 0 calc(-0.5ex - 1px);
left: calc(var(--percent) * 1%);
white-space: nowrap;
transform: translateY(calc(var(--y, 0) * -1.1em));
transition: transform 0.3s;
}
.graph-dd::before {
content: "";
display: inline-block;
background: blue;
width: 1ex;
height: 1ex;
vertical-align: baseline;
border-radius: 100%;
margin-right: 0.2em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<figure class="graph">
<dl class="graph-dl">
<dt class="graph-dt"><span>0</span></dt>
<dd class="graph-dd" data-value="9" style="--percent: 9; --y: 1;">Zaccaria Osmant</dd>
<dt class="graph-dt"><span>10</span></dt>
<dd class="graph-dd" data-value="11" style="--percent: 11; --y: 1;">Nelli Dunge</dd>
<dd class="graph-dd" data-value="12" style="--percent: 12; --y: 1;">Ethelind Evers</dd>
<dd class="graph-dd" data-value="16" style="--percent: 16; --y: 1;">Juliet Francis</dd>
<dt class="graph-dt"><span>20</span></dt>
<dt class="graph-dt"><span>30</span></dt>
<dt class="graph-dt"><span>40</span></dt>
<dd class="graph-dd" data-value="48" style="--percent: 48; --y: 1;">Myles Burdoun</dd>
<dt class="graph-dt"><span>50</span></dt>
<dd class="graph-dd" data-value="55" style="--percent: 55; --y: 1;">Gregory Beade</dd>
<dt class="graph-dt"><span>60</span></dt>
<dd class="graph-dd" data-value="60" style="--percent: 60; --y: 1;">Trenna Vigne</dd>
<dd class="graph-dd" data-value="61" style="--percent: 61; --y: 1;">Dulcia Koubu</dd>
<dt class="graph-dt"><span>70</span></dt>
<dd class="graph-dd" data-value="70" style="--percent: 70; --y: 1;">Amberly Wrightham</dd>
<dd class="graph-dd" data-value="73" style="--percent: 73; --y: 1;">Barney Rawstorn</dd>
<dt class="graph-dt"><span>80</span></dt>
<dt class="graph-dt"><span>90</span></dt>
<dd class="graph-dd" data-value="91" style="--percent: 91; --y: 1;">Nealson Helstrip</dd>
<dd class="graph-dd" data-value="92" style="--percent: 92; --y: 1;">Asa Langwade</dd>
<dd class="graph-dd" data-value="93" style="--percent: 93; --y: 1;">Malvin Imlaw</dd>
<dd class="graph-dd" data-value="96" style="--percent: 96; --y: 1;">Joanie Clooney</dd>
<dt class="graph-dt"><span>100</span></dt>
<dd class="graph-dd" data-value="100" style="--percent: 100; --y: 1;">Kristo Biskupski</dd>
</dl>
</figure>
<figure class="graph">
<dl class="graph-dl">
<dt class="graph-dt"><span>0</span></dt>
<dt class="graph-dt"><span>10</span></dt>
<dt class="graph-dt"><span>20</span></dt>
<dt class="graph-dt"><span>30</span></dt>
<dt class="graph-dt"><span>40</span></dt>
<dd class="graph-dd" data-value="44" style="--percent: 44; --y: 1;">Nelli Dunge</dd>
<dd class="graph-dd" data-value="48" style="--percent: 48; --y: 1;">Myles Burdoun</dd>
<dt class="graph-dt"><span>50</span></dt>
<dd class="graph-dd" data-value="51" style="--percent: 51; --y: 1;">Zaccaria Osmant</dd>
<dt class="graph-dt"><span>60</span></dt>
<dd class="graph-dd" data-value="61" style="--percent: 61; --y: 1;">Trenna Vigne</dd>
<dd class="graph-dd" data-value="61" style="--percent: 61; --y: 1;">Dulcia Koubu</dd>
<dd class="graph-dd" data-value="65" style="--percent: 65; --y: 1;">Ethelind Evers</dd>
<dt class="graph-dt"><span>70</span></dt>
<dd class="graph-dd" data-value="70" style="--percent: 70; --y: 1;">Amberly Wrightham</dd>
<dd class="graph-dd" data-value="73" style="--percent: 73; --y: 1;">Barney Rawstorn</dd>
<dd class="graph-dd" data-value="74" style="--percent: 74; --y: 1;">Kristo Biskupski</dd>
<dd class="graph-dd" data-value="75" style="--percent: 75; --y: 1;">Joanie Clooney</dd>
<dd class="graph-dd" data-value="77" style="--percent: 77; --y: 1;">Juliet Francis</dd>
<dd class="graph-dd" data-value="77" style="--percent: 77; --y: 1;">Gregory Beade</dd>
<dd class="graph-dd" data-value="79" style="--percent: 79; --y: 1;">Malvin Imlaw</dd>
<dt class="graph-dt"><span>80</span></dt>
<dd class="graph-dd" data-value="85" style="--percent: 85; --y: 1;">Asa Langwade</dd>
<dt class="graph-dt"><span>90</span></dt>
<dd class="graph-dd" data-value="91" style="--percent: 91; --y: 1;">Nealson Helstrip</dd>
<dt class="graph-dt"><span>100</span></dt>
</dl>
</figure>
There are some problems with your code:
const collides = ($e1, $e2) => {
let e1x1 = $e1.offset().left
let e1x2 = e1x1.x1 + $e1.outerWidth( true )
let e2x1 = $e2.offset().left
let e2x2 = e2x1.x1 + $e2.outerWidth( true )
let x = ((e1x1 < e2x1) && (e2x1 < e1x2)) || ((e2x1 < e1x1) && (e1x1 < e2x2))
let y = parseInt($e1.css('--y'), 10) === parseInt($e2.css('--y'), 10)
return !!(x || y)
}
collides
function, e1x1
and e2x1
is already a number so
e1x1.x1
and e2x1.x1
will be undefined. That's cause e1x2
and
e2x2
become NaN
.Juliet Francis
collide with Myles Burdoun
in the first graph). And because of that, you don't need to check for y value anymore.This is a modified collides
function:
const collides = ($e1, $e2) => {
let e1x1 = $e1.offset().left;
let e2x1 = $e2.offset().left;
let e1Width = $e1.outerWidth( true );
let e2Width = $e2.outerWidth( true );
let e1x2 = e1x1 + e1Width;
let e2x2 = e2x1 + e2Width;
if (e2x2 >= e1x1) return (e2x2 - e1x1) < (e1Width + e2Width);
else return (e1x2 - e2x1) < (e1Width + e2Width);
}
I also change how to calculte --yMax
of the graph to adopt with that modification.
UPDATE: I've misunderstood a bit. If you want an item to sit at the baseline if it had enough room, you can keep your logic, just need to make a little modification in collides
function and --yMax
calculation like the codepen below: