# Find closest elements in an array relative to given index with a bias to distance from index and direction

This function determines which images should be loaded based on which image is currently in the viewport (array index), what direction the user is scrolling, and if the image has yet to be loaded. It has a bias for what direction the user is scrolling, and proximity to the current image in viewport (delta). It attempts to match the two closest elements before and after.

The logic is mostly a set of ugly if statements, which I hope could be improved upon. Perhaps here is an opportunity for me to learn and implement a new design pattern or algorithm?

Basic example of matches.

Legend: _ = images, 0 = array index (image in viewport), X = matches (image to load)

0 X _ _ _ _ _
0 X X _ _ _ _
X 0 X _ _ _ _
X X 0 X X _ _
_ X X 0 X X _
_ _ X X 0 X X
_ _ _ X X 0 X
_ _ _ _ X X 0
_ _ _ _ _ X 0


Above, it will match the two closest elements (if available) before and after the index given the delta (distance) from the index is the same for the before and after elements.

If the user is scrolling, there is a bias towards the direction they are scrolling if the delta between the before and after elements is not equal. Eg. Using 30% bias, If scrolling right, and the closest right-side elements are 12 and 13 units away, and the closest left-side element is 10 units away we only load the 12 and 13 elements.

Y = Miss (closets element on respective before/after, but not matched due to scroll bias), --> = scroll direction

-->  _ _ _ _ _ _ X X 0 X X _ _ _ _ _ _ _
-->  _ _ _ _ Y Y _ _ _ 0 _ X X _ _ _ _ _
-->  _ _ _ _ Y Y _ _ _ _ 0 _ _ X X _ _ _
-->  _ _ _ _ Y Y _ _ _ _ 0 _ _ _ _ X X _
<--  _ _ _ _ X X _ _ _ 0 _ _ _ _ _ _ _ Y
<--  _ _ X X _ _ _ _ 0 _ _ _ _ _ _ _ _ Y
<--  X X _ _ _ _ _ _ 0 _ _ _ _ _ _ _ _ Y


Code:

queueClosestComponent(){
//find our location in the array
let lastIndex = this.components.indexOf(this.lastComponentInViewport);

//creates an array of the closet items and their relative position to the last image in the viewport
let mapped = this.components
.map(function(item, index) {
//map the closeness of each item
return {
delta: Math.abs(lastIndex - index),
component: item,
position: Math.sign(lastIndex - index) === 1 ? "previous" : "next"
};
}, this)
.filter(function(item) {
return true;
}
})
.sortBy("delta");

let closestNext = mapped.filterBy("position", "next").firstObject;
let closestPrevious = mapped.filterBy("position", "previous").firstObject;

//second closest
let closest2ndNext = mapped.filterBy("position", "next").objectAt(1);
let closest2ndPrevious = mapped
.filterBy("position", "previous")
.objectAt(1);

//if there is no next or previous
if (!closestNext && !closestPrevious) {
return;
}

//if there is no previous
if (closestNext && !closestPrevious) {

if (closest2ndNext)

return;
} else if (!closestNext && closestPrevious) {
//if there is no next

if (closest2ndPrevious)

return;
}

//if next and previous are the same delta, and within 3 items away
if (
closestNext.delta === closestPrevious.delta &&
closestNext.delta <= 3
) {

return;
}

//when scrolling, give a preference to the item in the given direction, but regardless still try to load the closest
let directionalFavour = 1.3; //30% favour to the given direction

if (
closestNext.delta / directionalFavour <= closestPrevious.delta &&
this.direction === "next"
) {
if (closest2ndNext)

return;
} else if (
closestNext.delta / directionalFavour >= closestPrevious.delta &&
this.direction === "next"
) {

return;
}

if (
closestNext.delta >= closestPrevious.delta / directionalFavour &&
this.direction === "previous"
) {
if (closest2ndPrevious)
return;
} else if (
closestNext.delta <= closestPrevious.delta / directionalFavour &&
this.direction === "previous"
) {
return;
}

}


There are some things we can improve even while keeping the same algorithm:

• DRY repeated/inverted conditions
• use a boolean isNext field instead of position string
• avoid creating four intermediate arrays to find closest items by using find() instead of filter()
• use arrow functions
• reduce the number of return statements to just one
class Foo {
queueClosestComponent() {
const lastIndex = this.components.indexOf(this.lastComponentInViewport);

const mapped = this.components
.map((component, index) => ({
delta: Math.abs(lastIndex - index),
isNext: index >= lastIndex,
component,
}))
.sortBy('delta');

const next = mapped.find(m => m.isNext);
const prev = mapped.find(m => !m.isNext);
if (!next && !prev) {
return;
}
const secondNext = mapped.find(m => m !== next && m.isNext);
const secondPrev = mapped.find(m => m !== prev && !m.isNext);

const directionalFavour = 1.3;

if (next && !prev) {
} else if (!next && prev) {
} else if (next.delta === prev.delta && next.delta <= 3) {
} else if (this.direction === 'next') {
toAdd = next.delta / directionalFavour <= prev.delta
? [next, secondNext]
: [prev];
} else if (this.direction === 'previous') {
toAdd = prev.delta / directionalFavour <= next.delta
? [prev, secondPrev]
: [next];
}