Search code examples
javascriptalgorithmmasonry

How to improve the vertical-only Masonry grid algorithm?


I've just made the simple core algorithm for the Vertical-only Masonry grid that takes an array of items data and creates a required amount of columns where items are sorted to "use" as much available space as possible. I'm not sure how to explain it in a most detailed way but in general, in every column total sum of the height of the nested items should be equal to others as much as possible.

Here is an example:

The source array of all unsorted items

// represents an array of data that will be a base to render UI elements
const items = [{
        "id": 0,
        "height": 100
    },
    {
        "id": 1,
        "height": 200
    },
    {
        "id": 2,
        "height": 250
    },
    {
        "id": 3,
        "height": 110
    },
    {
        "id": 4,
        "height": 50
    },
    {
        "id": 5,
        "height": 160
    },
    {
        "id": 6,
        "height": 70
    },
    {
        "id": 7,
        "height": 90
    }
]

As the result, I need to receive 3 columns but this value is dynamic so 3 is just an example. These columns should contain sorted items by height to use the most available space like this:

[
    [
        {
            "id": 0,
            "height": 100
        },
        {
            "id": 3,
            "height": 110
        },
        {
            "id": 5,
            "height": 160
        }
    ],
    [
        {
            "id": 1,
            "height": 200
        },
        {
            "id": 4,
            "height": 50
        },
        {
            "id": 7,
            "height": 90
        }
    ],
    [
        {
            "id": 2,
            "height": 250
        },
        {
            "id": 6,
            "height": 70
        }
    ]
]

As you can see the total sum of the columns is "most the same as possible":
1 col = 370
2 column = 340
3 column = 320

I implemented a solution but I'm wondering if there exists some better way how to solve this problem: Here is the link to JSFiddle and below the full source code of it:

I'll be appreciated for any idea/example how to improve it

// need to create 3 arrays sorted by height. Each array should contain ~equal height as much as possible
const requiredArrays = 3;

// represents an array of data that will be a base to render UI elements
const items = [{
        "id": 0,
        "height": 100
    },
    {
        "id": 1,
        "height": 200
    },
    {
        "id": 2,
        "height": 250
    },
    {
        "id": 3,
        "height": 110
    },
    {
        "id": 4,
        "height": 50
    },
    {
        "id": 5,
        "height": 160
    },
    {
        "id": 6,
        "height": 70
    },
    {
        "id": 7,
        "height": 90
    }
]




const cols = Array.from({
    length: requiredArrays
}, () => []);


// it sorts the columns by most "empty" or "most smallest sum height" and pushes the items into it to use as much available space as possible
function sorter(item) {
    let lowest = Number.POSITIVE_INFINITY;
    let highest = Number.NEGATIVE_INFINITY;
    let tmp;
    // the column where sum of its items is the lowest
    let mostEmptyCol;

    const colsDataWithTotalH = [];

    cols.forEach(col => {
        const totalH = col.reduce((acc, o) => acc + o.height, 0);
        // calculates the items sum of the single columns
        colsDataWithTotalH.push({
            col: col,
            totalH: totalH
        })
    })

    // looking for the most "empty" column by height
    for (var i = colsDataWithTotalH.length - 1; i >= 0; i--) {
        const currentCoItem = colsDataWithTotalH[i];
        tmp = currentCoItem.totalH;
        if (tmp < lowest) {
            lowest = tmp;
            // lets assign the Col array into this var to use it in future
            mostEmptyCol = currentCoItem.col;
        };
        if (tmp > highest) highest = tmp;
    }

    // fill the most empty column
    mostEmptyCol.push(item)
}


items.forEach(item => {

    const col = cols.find(o => {
        return !o.length;
    });

    // at the start columns are empty so we should just push items into them
    if (col) {
        col.push(item);
    } else {
        // the columns contain the items so we need to run the sorter algorhytm
        sorter(item);
    }

});

console.log('Result', cols);

Solution

  • Here's one approach:

    const sumHeights = (ns) =>
      ns.reduce ((a, {height}) => a + height, 0)
    
    const _equalGroups = ([x, ... xs], [g, ... gs]) =>
      x == undefined
        ? [g, ... gs]
      : _equalGroups (
          xs, 
          [[... g, x], ... gs] .sort ((as, bs) => sumHeights (as) - sumHeights (bs))
        )
    
    const equalGroups = (n, xs) => 
      _equalGroups (xs .sort ((a, b) => b.height - a.height), [...Array(n)] .map (_ => []))
    
    const items = [{id: 0, height: 100}, {id: 1, height: 200}, {id: 2, height: 250}, {id: 3, height: 110}, {id: 4, height: 50}, {id: 5, height: 160}, {id: 6, height: 70}, {id: 7, height: 90}]
    
    console .log (equalGroups (3, items))
    .as-console-wrapper {max-height: 100% !important; top: 0}

    This is not an optimizing solution, just a rough fit. I'm guessing that any optimizing solution would also solve many versions of the Knapsack problem, so I'm not going there!

    The idea is simple. We have a helper function to sum the heights of the elements of an array, and we have our public function equalGroups, which accepts the number of groups and an array of items, then simply calls our main recursive function _equalGroups, passing it an array of items sorted descending by their height and the appropriate number of empty arrays.

    The main function stops when we have no more items to insert. At all other times, it stores the next number in the first group, then recurs on the remaining numbers and the updated collection of groups, sorted by size.