Visualization of calendar events. Algorithm to layout events with maximum width - javascript

I need your help with an algorithm (it will be developed on client side with javascript, but doesn't really matter, I'm mostly interested in the algorithm itself) laying out calendar events so that each event box has maximum width. Please see the following picture:
Y axis is time. So if "Test event" starts at noon (for example) and nothing more intersects with it, it takes the whole 100% width. "Weekly review" intersects with "Tumbling YMCA" and "Anna/Amelia", but the latter two are not intersecting, so they all fill up 50%. Test3, Test4 and Test5 are all intersecting, so max width is 33.3% for each. But Test7 is 66% since Test3 is 33% fixed (see above) , so it takes all available space , which is 66%.
I need an algorithm how to lay this out.
Thanks in advance

Think of an unlimited grid with just a left edge.
Each event is one cell wide, and the height and vertical position is fixed based on starting and ending times.
Try to place each event in a column as far left as possible, without it intersecting any earlier event in that column.
Then, when each connected group of events is placed, their actual widths will be 1/n of the maximum number of columns used by the group.
You could also expand the events at the far left and right to use up any remaining space.
/// Pick the left and right positions of each event, such that there are no overlap.
/// Step 3 in the algorithm.
void LayoutEvents(IEnumerable<Event> events)
{
var columns = new List<List<Event>>();
DateTime? lastEventEnding = null;
foreach (var ev in events.OrderBy(ev => ev.Start).ThenBy(ev => ev.End))
{
if (ev.Start >= lastEventEnding)
{
PackEvents(columns);
columns.Clear();
lastEventEnding = null;
}
bool placed = false;
foreach (var col in columns)
{
if (!col.Last().CollidesWith(ev))
{
col.Add(ev);
placed = true;
break;
}
}
if (!placed)
{
columns.Add(new List<Event> { ev });
}
if (lastEventEnding == null || ev.End > lastEventEnding.Value)
{
lastEventEnding = ev.End;
}
}
if (columns.Count > 0)
{
PackEvents(columns);
}
}
/// Set the left and right positions for each event in the connected group.
/// Step 4 in the algorithm.
void PackEvents(List<List<Event>> columns)
{
float numColumns = columns.Count;
int iColumn = 0;
foreach (var col in columns)
{
foreach (var ev in col)
{
int colSpan = ExpandEvent(ev, iColumn, columns);
ev.Left = iColumn / numColumns;
ev.Right = (iColumn + colSpan) / numColumns;
}
iColumn++;
}
}
/// Checks how many columns the event can expand into, without colliding with
/// other events.
/// Step 5 in the algorithm.
int ExpandEvent(Event ev, int iColumn, List<List<Event>> columns)
{
int colSpan = 1;
foreach (var col in columns.Skip(iColumn + 1))
{
foreach (var ev1 in col)
{
if (ev1.CollidesWith(ev))
{
return colSpan;
}
}
colSpan++;
}
return colSpan;
}
Edit: Now sorts the events, instead of assuming they is sorted.
Edit2: Now expands the events to the right, if there are enough space.

The accepted answer describes an algorithm with 5 steps. The example implementation linked in the comments of the accepted answer implements only steps 1 to 4. Step 5 is about making sure the rightmost event uses all the space available. See event 7 in the image provided by the OP.
I expanded the given implementation by adding step 5 of the described algorithm:
$( document ).ready( function( ) {
var column_index = 0;
$( '#timesheet-events .daysheet-container' ).each( function() {
var block_width = $(this).width();
var columns = [];
var lastEventEnding = null;
// Create an array of all events
var events = $('.bubble_selector', this).map(function(index, o) {
o = $(o);
var top = o.offset().top;
return {
'obj': o,
'top': top,
'bottom': top + o.height()
};
}).get();
// Sort it by starting time, and then by ending time.
events = events.sort(function(e1,e2) {
if (e1.top < e2.top) return -1;
if (e1.top > e2.top) return 1;
if (e1.bottom < e2.bottom) return -1;
if (e1.bottom > e2.bottom) return 1;
return 0;
});
// Iterate over the sorted array
$(events).each(function(index, e) {
// Check if a new event group needs to be started
if (lastEventEnding !== null && e.top >= lastEventEnding) {
// The latest event is later than any of the event in the
// current group. There is no overlap. Output the current
// event group and start a new event group.
PackEvents( columns, block_width );
columns = []; // This starts new event group.
lastEventEnding = null;
}
// Try to place the event inside the existing columns
var placed = false;
for (var i = 0; i < columns.length; i++) {
var col = columns[ i ];
if (!collidesWith( col[col.length-1], e ) ) {
col.push(e);
placed = true;
break;
}
}
// It was not possible to place the event. Add a new column
// for the current event group.
if (!placed) {
columns.push([e]);
}
// Remember the latest event end time of the current group.
// This is later used to determine if a new groups starts.
if (lastEventEnding === null || e.bottom > lastEventEnding) {
lastEventEnding = e.bottom;
}
});
if (columns.length > 0) {
PackEvents( columns, block_width );
}
});
});
// Function does the layout for a group of events.
function PackEvents( columns, block_width )
{
var n = columns.length;
for (var i = 0; i < n; i++) {
var col = columns[ i ];
for (var j = 0; j < col.length; j++)
{
var bubble = col[j];
var colSpan = ExpandEvent(bubble, i, columns);
bubble.obj.css( 'left', (i / n)*100 + '%' );
bubble.obj.css( 'width', block_width * colSpan / n - 1 );
}
}
}
// Check if two events collide.
function collidesWith( a, b )
{
return a.bottom > b.top && a.top < b.bottom;
}
// Expand events at the far right to use up any remaining space.
// Checks how many columns the event can expand into, without
// colliding with other events. Step 5 in the algorithm.
function ExpandEvent(ev, iColumn, columns)
{
var colSpan = 1;
// To see the output without event expansion, uncomment
// the line below. Watch column 3 in the output.
//return colSpan;
for (var i = iColumn + 1; i < columns.length; i++)
{
var col = columns[i];
for (var j = 0; j < col.length; j++)
{
var ev1 = col[j];
if (collidesWith(ev, ev1))
{
return colSpan;
}
}
colSpan++;
}
return colSpan;
}
A working demo is available at http://jsbin.com/detefuveta/edit?html,js,output
See column 3 of the output for examples of expanding the rightmost events.
PS: This should really be a comment to the accepted answer. Unfortunately I don't have the privileges to comment.

Here's the same algorithm implemented for React using Typescript. You'll have to tweak it to fit your needs (of course), but it should prove useful for anyone working in React:
// Place concurrent meetings side-by-side (like GCal).
// #see {#link https://share.clickup.com/t/h/hpxh7u/WQO1OW4DQN0SIZD}
// #see {#link https://stackoverflow.com/a/11323909/10023158}
// #see {#link https://jsbin.com/detefuveta/edit}
// Check if two events collide (i.e. overlap).
function collides(a: Timeslot, b: Timeslot): boolean {
return a.to > b.from && a.from < b.to;
}
// Expands events at the far right to use up any remaining
// space. Returns the number of columns the event can
// expand into, without colliding with other events.
function expand(
e: Meeting,
colIdx: number,
cols: Meeting[][]
): number {
let colSpan = 1;
cols.slice(colIdx + 1).some((col) => {
if (col.some((evt) => collides(e.time, evt.time)))
return true;
colSpan += 1;
return false;
});
return colSpan;
}
// Each group contains columns of events that overlap.
const groups: Meeting[][][] = [];
// Each column contains events that do not overlap.
let columns: Meeting[][] = [];
let lastEventEnding: Date | undefined;
// Place each event into a column within an event group.
meetings
.filter((m) => m.time.from.getDay() === day)
.sort(({ time: e1 }, { time: e2 }) => {
if (e1.from < e2.from) return -1;
if (e1.from > e2.from) return 1;
if (e1.to < e2.to) return -1;
if (e1.to > e2.to) return 1;
return 0;
})
.forEach((e) => {
// Check if a new event group needs to be started.
if (
lastEventEnding &&
e.time.from >= lastEventEnding
) {
// The event is later than any of the events in the
// current group. There is no overlap. Output the
// current event group and start a new one.
groups.push(columns);
columns = [];
lastEventEnding = undefined;
}
// Try to place the event inside an existing column.
let placed = false;
columns.some((col) => {
if (!collides(col[col.length - 1].time, e.time)) {
col.push(e);
placed = true;
}
return placed;
});
// It was not possible to place the event (it overlaps
// with events in each existing column). Add a new column
// to the current event group with the event in it.
if (!placed) columns.push([e]);
// Remember the last event end time of the current group.
if (!lastEventEnding || e.time.to > lastEventEnding)
lastEventEnding = e.time.to;
});
groups.push(columns);
// Show current time indicator if today is current date.
const date = getDateWithDay(day, startingDate);
const today =
now.getFullYear() === date.getFullYear() &&
now.getMonth() === date.getMonth() &&
now.getDate() === date.getDate();
const { y: top } = getPosition(now);
return (
<div
key={nanoid()}
className={styles.cell}
ref={cellRef}
>
{today && (
<div style={{ top }} className={styles.indicator}>
<div className={styles.dot} />
<div className={styles.line} />
</div>
)}
{groups.map((cols: Meeting[][]) =>
cols.map((col: Meeting[], colIdx) =>
col.map((e: Meeting) => (
<MeetingItem
now={now}
meeting={e}
viewing={viewing}
setViewing={setViewing}
editing={editing}
setEditing={setEditing}
setEditRndVisible={setEditRndVisible}
widthPercent={
expand(e, colIdx, cols) / cols.length
}
leftPercent={colIdx / cols.length}
key={e.id}
/>
))
)
)}
</div>
);
You can see the full source-code here. I'll admit that this is a highly opinionated implementation, but it would've helped me so I'll post it here to see if it helps anyone else!

Related

Fill a 6x6 grid with 6 colors without same colors touching each other

I'm trying to create a board game with p5.js (Javascript)
To set up the game board which is a 6 by 6 grid, I have to fill the grid with 6 colors in a way that no horizontal or vertical touching cells have the same color. And all 6 colors have to be used in 6 cells.
But now I'm struggling a bit creating an algorithm that places the colors randomly but keeping the rules.
I tried to start at the top left corner, filling with a random color.
Then I start to fill the cell to the left and the bottom with a different color.
The problem is, that when the script wants to fill the last few cells, there are no colors left to use (either already 6 cells filled or a remaining color is a neighbor)
Example:
Still two cells need to be red, but only one place is left for red (under white):
//fill placedColors Array with zeros
placedColors = [];
for(let i=0; i<6; i++) {
placedColors[i] = 0;
}
//fill allIndexes Array with indizies to keep control of visited cells
let allIndexes = [];
for(let i=0; i<36; i++) {
allIndexes.push(i);
}
//build board
//when I set the limit to 36 the script runs forever because no solution is found
for(let i=0; i<33; i++) {
fillCells(i);
}
function fillCells(index) {
//get top and left color
let topColor = false;
//index is in the second row
if(index >= 6) {
topColor = cells[index-6].color;
}
let leftColor = false;
//index is not in the first column
if(index % 6 > 0 && index > 0) {
leftColor = cells[index-1].color;
}
if(allIndexes.indexOf(index) > -1) {
cells.push(new Cell(index, pickColor(topColor, leftColor)));
}
//mark index as visited
var allIndexesIndex = allIndexes.indexOf(index);
if (allIndexesIndex !== -1) {
allIndexes.splice(allIndexesIndex, 1);
}
}
function pickColor(invalidColor1,invalidColor2) {
let colorFound = false;
do {
randColor = floor(random(6));
if(placedColors[randColor] < 6 && randColor!=invalidColor1 && randColor!=invalidColor2) {
placedColors[randColor]++;
colorFound = true;
}
} while(!colorFound);
return randColor;
}
One way to look at this would be as searching for a path through a tree where each node has 6 possible children for the six colours which could come next. Ignoring all the constraints initially, you pick one of these at random 36 times, and have your order of placements.
Using a recursive function (which will be useful in a moment), an unconstrained search would look like this:
function randomChoice(list) {
return list[Math.floor(Math.random() * list.length)];
}
function placeNext(sequenceSoFar) {
const availableColours = [0,1,2,3,4,5];
let chosenColour = randomChoice(availableColours);
sequenceSoFar = [...sequenceSoFar, colourToAdd];
if ( sequenceSoFar.length == 36 ) {
// Completed sequence!
return sequenceSoFar;
}
else {
// Recurse to make next choice
return placeNext(sequenceSoFar);
}
}
// Start with an empty array
let sequence = placeNext([]);
console.log(sequence);
Next, we need to eliminate choices that would violate the constraints of the problem:
The cell to the left isn't the same colour, i.e. chosenColour !== sequenceSoFar[nextIndex-1]
The cell above isn't the same colour, i.e. chosenColour !== sequenceSoFar[nextIndex-6]
The colour doesn't already occur six times in the sequence, i.e. sequenceSoFar.filter((element) => element === chosenColour).length < 6
If the chosen colour doesn't meet these requirements, we remove it from the list of candidates and try again:
function randomChoice(list) {
return list[Math.floor(Math.random() * list.length)];
}
function newColourIsValid(sequenceSoFar, chosenColour) {
// We haven't added the next colour yet, but know where it *will* be
let nextIndex = sequenceSoFar.length;
return (
// The cell to the left isn't the same colour
( nextIndex < 1 || chosenColour !== sequenceSoFar[nextIndex-1] )
&&
// The cell above isn't the same colour
( nextIndex < 6 || chosenColour !== sequenceSoFar[nextIndex-6] )
&&
// The colour doesn't already occur six times in the sequence
sequenceSoFar.filter((element) => element === chosenColour).length < 6
);
}
function placeNext(sequenceSoFar) {
let availableColours = [0,1,2,3,4,5];
let chosenColour;
do {
// If we run out of possible colours, then ... panic?
if ( availableColours.length === 0 ) {
console.log(sequenceSoFar);
throw 'No sequence possible from here!';
}
chosenColour = randomChoice(availableColours);
// Eliminate the colour from the list of choices for next iteration
availableColours = availableColours.filter((element) => element !== chosenColour);
} while ( ! newColourIsValid(sequenceSoFar, chosenColour) );
sequenceSoFar = [...sequenceSoFar, colourToAdd];
if ( sequenceSoFar.length == 36 ) {
// Completed sequence!
return sequenceSoFar;
}
else {
// Recurse to make next choice
return placeNext(sequenceSoFar);
}
}
// Start with an empty array
let sequence = placeNext([]);
console.log(sequence);
So far, this has the same problem as your original code - if it hits a dead end, it doesn't know what to do. To solve this, we can use a backtracking algorithm. The idea is that if we run out of candidates for position n, we reject the choice at position n-1 and try a different one.
Instead of placeNext, we need our function to be tryPlaceNext, which can return false if the sequence hits a dead end:
function randomChoice(list) {
return list[Math.floor(Math.random() * list.length)];
}
function newColourIsValid(sequenceSoFar, chosenColour) {
// We haven't added the next colour yet, but know where it *will* be
let nextIndex = sequenceSoFar.length;
return (
// The cell to the left isn't the same colour
( nextIndex < 1 || chosenColour !== sequenceSoFar[nextIndex-1] )
&&
// The cell above isn't the same colour
( nextIndex < 6 || chosenColour !== sequenceSoFar[nextIndex-6] )
&&
// The colour doesn't already occur six times in the sequence
sequenceSoFar.filter((element) => element === chosenColour).length < 6
);
}
function tryPlaceNext(sequenceSoFar, colourToAdd) {
let availableColours = [0,1,2,3,4,5];
if ( ! newColourIsValid(sequenceSoFar, colourToAdd) ) {
// Invalid choice, indicate to caller to try again
return false;
}
// Valid choice, add to sequence, and find the next
sequenceSoFar = [...sequenceSoFar, colourToAdd];
if ( sequenceSoFar.length === 36 ) {
// Completed sequence!
return sequenceSoFar;
}
while ( availableColours.length > 0 ) {
// Otherwise, pick one and see if we can continue the sequence with it
let chosenColour = randomChoice(availableColours);
// Recurse to make next choice
let continuedSequence = tryPlaceNext(sequenceSoFar, chosenColour);
if ( continuedSequence !== false ) {
// Recursive call found a valid sequence, return it
return continuedSequence;
}
// Eliminate the colour from the list of choices for next iteration
availableColours = availableColours.filter((element) => element !== chosenColour);
}
// If we get here, we ran out of possible colours, so indicate a dead end to caller
return false;
}
// Root function to start an array with any first element
function generateSequence() {
let availableColours = [0,1,2,3,4,5];
let chosenColour = randomChoice(availableColours);
return tryPlaceNext([], chosenColour);
}
let sequence = generateSequence();
console.log(sequence);
Thanks for your suggestions! I tried to find an own solution parallel to the posted one. Now with this code, it works fine:
function buildBoard() {
cells = [];
for(let i=0; i<gameSize; i++) {
placedColors[i] = 0;
}
for(var i=0; i<gameSize*gameSize; i++) {
cells.push(new Cell(i, pickColor()));
}
do {
invalidFields = [];
findInvalidFields();
if(invalidFields.length > 0) {
let cell1index = Math.floor(Math.random() * invalidFields.length);
cell1 = invalidFields[cell1index];
//check, if cell in different color available
let otherColorAvailable = false;
for(cell of invalidFields) {
if(cell.color != cell1.color) {
otherColorAvailable = true;
break;
}
}
if(otherColorAvailable) {
//pick invalid cell
do {
let cell2index = Math.floor(Math.random() * invalidFields.length);
cell2 = invalidFields[cell2index];
} while (cell2.color == cell1.color)
} else {
//pick random cell
do {
let cell2index = Math.floor(Math.random() * cells.length);
cell2 = cells[cell2index];
} while (cell2.color == cell1.color)
}
//switch colors of cells
let tempColor = cell1.color;
cell1.color = cell2.color;
cell2.color = tempColor;
}
} while (!checkStartField());
}
function findInvalidFields() {
for(let index=0; index<cells.length; index++) {
let thisColor = cells[index].color;
//right
if(index%gameSize < gameSize-1 && cells[index+1].color == thisColor) {
if(invalidFields.indexOf(cells[index+1])) {
invalidFields.push(cells[index+1]);
}
}
//bottom
if(index < gameSize*gameSize-gameSize && cells[index+gameSize].color == thisColor) {
if(invalidFields.indexOf(cells[index+gameSize])) {
invalidFields.push(cells[index+gameSize]);
}
}
}
}
function checkStartField() {
for(let index=0; index<cells.length; index++) {
let thisColor = cells[index].color;
//top
if(index >= gameSize && cells[index-gameSize].color == thisColor) {
//console.log(index+'top');
return false;
}
//right
if(index%gameSize < gameSize-1 && cells[index+1].color == thisColor) {
//console.log(index+'right');
return false;
}
//bottom
if(index < gameSize*gameSize-gameSize && cells[index+gameSize].color == thisColor) {
//console.log(index+'bottom');
return false;
}
//left
if(index%gameSize > 0 && cells[index-1].color == thisColor) {
//console.log(index+'left');
return false;
}
}
return true;
}
An easy approach is to start with a valid coloring (for example, any 6x6 latin square is a valid coloring) and them mix it up by finding a pair of things that can be swapped, and swap them.
An improvement (to increase mixing speed, and to prevent the algorithm getting stuck) is to allow at most one cell to be invalid (ie: one cell which if removed leaves the remainder in a valid coloring). If there's no invalid cell, then swap two random cells if at least one of them will be valid after the swap. And if there's one invalid cell, pick that cell and one other random cell to be swapped, assuming again swapping leaves at least one of them valid. Again repeat lots of time, stopping only when the coloring is valid.
An implementation of this idea (sorry, not Javascript) is here: https://go.dev/play/p/sxMvLxHfhmC

Container With Most Water algorithm - using recursion

I am doing leetcode #11 Container With Most Water
https://leetcode.com/problems/container-with-most-water/
Given n non-negative integers a1, a2, ..., an , where each represents a point at coordinate (i, ai). n vertical lines are drawn such that the two endpoints of line i is at (i, ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.
Note: You may not slant the container and n is at least 2.
var maxArea = function (height) {
var list = [];
for (var index = 1; index <= height.length; index++) {
var eachCorr = {
x_corr: index,
y_corr: height[index - 1]
}
list.push(eachCorr);
}
var mainResult = reCursion(list, list.length-1,0,1);
console.log(list);
console.log(mainResult);
return mainResult;
//last vertical line * each vertical line from index=1;
//x-corr*(last vertical - each vertical), y-corr*(smaller vertical line)
};
function reCursion(arr, index, x,y) {
//lastX and lastY use recursion to loop
var lastX = arr[index][x];
var lastY = arr[index][y];
var chosenY = 0;
var area = 0;
var result = [];
var maxAreaAns = 0;
for (var i = index - 1; i >= 0; i--) {
if (lastY > arr[i][1]) {
chosenY = arr[i][1];
} else {
chosenY = lastY;
}
area = (lastX - arr[i][0]) * chosenY;
console.log(`area = ${area} with i = ${i}, lastX=${lastX}, lastY=${lastY}`);
result.push(area);
}
if (index === 0) {
maxAreaAns = Math.max(...result);
return maxAreaAns;
} else {
return reCursion(arr, index - 1,0,1);
}
}
My approach is using recursion, first select the last vertical line, multiple to x-corr difference of
each vertical line before, then select the small y-corr of the vertical line when compared.
area = (x-corr difference of last vertical line and compared vertical line) * (y-coor of small vertical line)
Then use recursion to select the second last vertical line and so all until select the first vertical line.
Then I push all the area result into a array and find the maximum.
I want to know why this method can not execute( lastX, lastY, area variables are undefined).
Having analyzed your code, your
var lastX = arr[index][x];
var lastY = arr[index][y];
are both always undefined. Since arr[index] returns an object and not a list, you cannot get the values by indexing. You'll need to do
var lastX = arr[index].x_corr;
var lastY = arr[index].y_corr;
Which also goes for your
if (lastY > arr[i][1]) {
chosenY = arr[i][1];
Now you might have realized that your function always logs out -Infinity as its result.
This is because when the condition
if (index === 0) {
maxAreaAns = Math.max(...result);
return maxAreaAns;
}
is met and the code inside it is executed, the result array is always empty (try invoking the Math.max() function without any input. It will return -Infinity).
This is because when the index variable is equal to 0, the loop
for (var i = index - 1; i >= 0; i--) {
if (lastY > arr[i][1]) {
...
}
will not run (as i starts from -1), and the result stays as an empty array.
I am guessing that what you would want to do is to either set result array as a global variable, or to pass it to the next reCursion() function.
That being said, I actually don't see the point of solving this problem using recursion.
Instead of using recursion (which obviously makes it difficult to write and understand the code), why not just use a nested loop to check the combinations?

How to determine overlaps between selection ranges in a grid of items in Javascript

Imagine something like a grid of items.
The user can select multiple ranges of items in the grid. When there are multiple ranges (selections), I want to determine wether the last created range overlaps with an existing range.
The grid system can be compared with characters in a textarea, where it is possible to highlight multiple text ranges.
Every single range always exists of adjacent items, next to each
other, or the first item in the next row in the grid. (Also totally
similar to selecting text in a text document.)
When the user creates a range, it can overlap with an existing range, or even totally fit into an existing range.
A range is stored in memory as:
{
'rangeStartLineIndex': 2,
'rangeStartColIndex': 9,
'rangeEndLineIndex': 4,
'rangeEndColIndex': 7
}
Above range can be visualized as in the image. But note that the number of rows and columns of the grid is not constant.
The goal is to loop through the existing ranges, and look if the just created range overlaps with (or totally fits in) an existing range. If so, then take that existing range, and extend it so the created range merges with the one it overlaps with. So, it's kind of normalizing data.
Another example in code:
var ranges = []; // stores the range objects that are created earlier.
var createdRange = {...}; // range object just created.
for(var i = 0; i < ranges; i++) {
var overlap = doesThisOverlap(createdRange, ranges[i]);
if(overlap) {
// overlaps, which means we extend the existing range.
range[i].rangeStartLineIndex = Math.min(range[i].rangeStartLineIndex, createdRange.rangeStartLineIndex);
range[i].rangeStartColIndex = Math.min(range[i].rangeStartColIndex, createdRange.rangeStartColIndex);
range[i].rangeEndLineIndex = Math.max(range[i].rangeEndLineIndex, createdRange.rangeEndLineIndex);
range[i].rangeEndColIndex = Math.max(range[i].rangeEndColIndex, createdRange.rangeEndColIndex);
} else {
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
}
function doesThisOverlap(rangeA, rangeB) {
// ???
}
When trying to implement the function doesThisOverlap, I end up with an excessive amount of if-blocks. I get confused, also because I've got the feeling there's an algorithm found for this.
What I also tried is adding up a range startpoint's line and col index to give it a 'score', (and do the same for its endpoint's line and column index).
Then compare that startpoint/endpoint scores between the ranges.
The problem is not really 2D, it becomes much easier if you represent the range as
{
rangeStart: 29,
rangeEnd:48
}
You can convert to this representation by calculating
lineIndex * COLUMN_NUMBER + columnIndex.
You should basically iterate all ranges and find min rangeStart and rangeEnd. Then you can convert the result to column/row using:
columnIndex = x % COLUMN_NUMBER;
lineIndex = parseInt(x / COLUMN_NUMBER).
One of the way to identify whether the createdRange overlaps with one of the ranges is to give each range start index and end index and then, check whether the indices of createdRange overlaps with indices of any other range or not.
First, Let us change the shape of the range object from better readability:
{
start: { row: 2, col: 9 },
end: { row: 4, col: 7 }
}
Mapping of this range object with the one you defined is simple:
rangeStartLineIndex => start.row
rangeStartColIndex => start.col
rangeEndLineIndex => end.row
rangeEndColIndex => end.col
With this out of the way, I would first point at one little mistake in the logic. In the for loop, you are checking if the createdRange overlaps with the current range or not. If not, you are adding that createdRange to the ranges array.
However, you only need to add the createdRange in the ranges IF none of the ranges overlap with createdRange
Thus, the correct for loop would look like:
var hasOverlap = false; // this will tell if any of the ranges overlap
for(var i = 0; i < ranges; i++) {
var overlap = doesThisOverlap(createdRange, ranges[i]);
if(overlap) {
// overlaps, which means we extend the existing range.
// some logic to update the overlapped ranges
hasOverlap = true; // a range has overlapped, set the flag to true
break;
}
}
// Did we see any overlap?
if(!hasOverlap) {
// no we did not, let us add this range to ranges array
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
Alright, now let us see how to calculate the indices for the given range.
If we start assigning indices (starting from 0) from left to right in the grid,
simple math says that the index of the box in the row r and in the column c will be:
index = r * (COL + 1) + c [COL is the total number of columns in the grid]
Here are the helper functions which will help to calculate the indices given the range:
function getIndex(row, col, COL) {
return row * (COL + 1) + col;
}
function getIndices(range) {
var start = range.start;
var end = range.end;
var startIndex = getIndex(start.row, start.col, COLS);
var endIndex = getIndex(end.row, end.col, COLS);
return { start: startIndex, end: endIndex };
}
Note that getIndices takes a range and outputs an object with start and end indices. We can now calculate indices for createdRange and current range. And based on the indices, we would know whether the ranges overlap or not.
The problem now boils down to this:
We have a line AB, and given a new line PQ, find out whether the new line PQ overlaps AB or not. ( where A,B,P,Q are points on the number line, A < B and P < Q ).
Take pen and paper and draw a few lines. You will come to know that there only two condition when the lines will not overlap:
Either Q < A or B < P
Mapping this observations to our range object, we can say that:
P => createdRange.startIndex
Q => createdRange.endIndex
A => currentRange.startIndex
B => currentRange.endIndex
This is how it would look in the code:
var createdRangeIndices = getIndices(createdRange);
var hasOverlap = false;
for (var i = 0; i < ranges.length; i++) {
var currentRangeIndices = getIndices(ranges[i]);
var overlap = (createdRangeIndices.end < currentRangeIndices.start)
|| (currentRangeIndices.end < createdRangeIndices.start);
if (!overlap) {
// overlaps, which means we extend the existing range.
// some logic to update the overlapped ranges
hasOverlap = true;
break;
}
}
if (!hasOverlap) {
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
Note that we got rid of the function doesThisOverlap. A simple flag would do.
All that remains now is the logic to update the range if there is an overlap.
A part of which you had already figured out in your question. We take the minimum of the starting index and maximum of the ending index. Here is the code for that:
for (var i = 0; i < ranges.length; i++) {
var currentRangeIndices = getIndices(ranges[i]);
var overlap = (createdRangeIndices.end < currentRangeIndices.start)
|| (currentRangeIndices.end < createdRangeIndices.start);
if (!overlap) {
// overlaps, which means we extend the existing range.
// some logic to update the overlapped ranges
var start, end;
if (currentRangeIndices.start < createdRangeIndices.start) {
start = ranges[i].start;
} else {
start = createdRange.start;
}
if (currentRangeIndices.end > createdRangeIndices.end) {
end = ranges[i].end;
} else {
end = createdRange.end;
}
ranges[i] = { start: start, end: end };
hasOverlap = true;
break;
}
}
And done!
Here is the complete code that combines all the bits and pieces together:
var ROWS = 7;
var COLS = 3;
function getIndex(row, col, COL) {
return row * (COL + 1) + col;
}
function getIndices(range) {
var start = range.start;
var end = range.end;
var startIndex = getIndex(start.row, start.col, COLS);
var endIndex = getIndex(end.row, end.col, COLS);
return { start: startIndex, end: endIndex };
}
function addRange(ranges, createdRange) {
var createdRangeIndices = getIndices(createdRange);
var hasOverlap = false;
for (var i = 0; i < ranges.length; i++) {
var currentRangeIndices = getIndices(ranges[i]);
var overlap =
createdRangeIndices.end < currentRangeIndices.start ||
currentRangeIndices.end < createdRangeIndices.start;
if (!overlap) {
var start, end;
if (currentRangeIndices.start < createdRangeIndices.start) {
start = ranges[i].start;
} else {
start = createdRange.start;
}
if (currentRangeIndices.end > createdRangeIndices.end) {
end = ranges[i].end;
} else {
end = createdRange.end;
}
ranges[i] = { start: start, end: end };
hasOverlap = true;
break;
}
}
if (!hasOverlap) {
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
}
var ranges = []; // stores the range objects that are created earlier.
var rangesToAdd = [
{
start: { row: 2, col: 1 },
end: { row: 6, col: 0 }
},
{
start: { row: 6, col: 2 },
end: { row: 7, col: 2 }
},
{
start: { row: 3, col: 1 },
end: { row: 6, col: 1 }
},
{
start: { row: 6, col: 1 },
end: { row: 6, col: 2 }
}
];
rangesToAdd.forEach(aRange => addRange(ranges, aRange));
console.log(ranges);

JavaScript game starts fast and slows down over time

My simple JavaScript game is a Space Invaders clone.
I am using the p5.js client-side library.
I have tried many things to attempt at speeding up the game.
It start off fast, and then over time it get slower, and slower, it isn't as enjoyable.
I do not mean to show every bit of code I have. I am not showing every class, but I will show the main class where everything is happening.
Could someone eyeball this and tell me if you see anything major?
I am new to JS and new to making games, I know there is something called update()
that people use in scripting but I am not familiar with it.
Thank you.
var ship;
var flowers = []; // flowers === aliens
var drops = [];
var drops2 = [];
function setup() {
createCanvas(600, 600);
ship = new Ship();
for (var i = 0; i < 6; i ++) {
flowers[i] = new Flower(i * 80 + 80, 60);
}
flower = new Flower();
}
function draw() {
background(51);
ship.show();
ship.move();
shipDrops();
alienDrops();
dropsAndAliens();
dropDelete();
drop2Delete();
}
// if 0 drops, show and move none, if 5, etc..
function shipDrops() {
for (var i = 0; i < drops.length; i ++) {
drops[i].show();
drops[i].move();
for (var j = 0; j < flowers.length; j++) {
if(drops[i].hits(flowers[j]) ) {
flowers[j].shrink();
if (flowers[j].r === 0) {
flowers[j].destroy();
}
// get rid of drops after it encounters ship
drops[i].evaporate();
}
if(flowers[j].toDelete) {
// if this drop remove, use splice function to splice out of array
flowers.splice(j, 1); // splice out i, at 1
}
}
}
}
function alienDrops() {
// below is for alien/flower fire drops 2
for (var i = 0; i < drops2.length; i ++) {
drops2[i].show();
drops2[i].move();
if(drops2[i].hits(ship) ) {
ship.shrink();
drops2[i].evaporate(); // must evap after shrink
ship.destroy();
if (ship.toDelete) {
delete ship.x;
delete ship.y;
} // above is in progress, deletes after ten hits?
}
}
}
function dropsAndAliens() {
var randomNumber; // for aliens to shoot
var edge = false;
// loop to show multiple flowers
for (var i = 0; i < flowers.length; i ++) {
flowers[i].show();
flowers[i].move();
// ******************************************
randomNumber = Math.floor(Math.random() * (100) );
if(randomNumber === 5) {
var drop2 = new Drop2(flowers[i].x, flowers[i].y, flowers[i].r);
drops2.push(drop2);
}
//**************** above aliens shooting
// below could be method, this will ensure the flowers dont
//go offscreen and they move
//makes whtever flower hits this space become the farther most
//right flower,
if (flowers[i].x > width || flowers[i]. x < 0 ) {
edge = true;
}
}
// so if right is true, loop thru them all again and reset x
if (edge) {
for (var i = 0; i < flowers.length; i ++) {
// if any flower hits edge, all will shift down
// and start moving to the left
flowers[i].shiftDown();
}
}
}
function dropDelete() {
for (var i = drops.length - 1; i >= 0; i--) {
if(drops[i].toDelete) {
// if this drop remove, use splice function to splice out of array
drops.splice(i, 1); // splice out i, at 1
}
}
}
function drop2Delete() {
for (var i = drops2.length - 1; i >= 0; i--) {
if(drops2[i].toDelete) {
// if this drop remove, use splice function to splice out of array
drops2.splice(i, 1); // splice out i, at 1
}
}
}
function keyReleased() {
if (key != ' ') {
ship.setDir(0); // when i lift the key, stop moving
}
}
function keyPressed() {
// event triggered when user presses key, check keycode
if(key === ' ') {
var drop = new Drop(ship.x, height); // start ship x and bottom of screen
drops.push(drop); // when user hits space, add this event to array
}
if (keyCode === RIGHT_ARROW) {
// +1 move right
ship.setDir(1);
} else if (keyCode === LEFT_ARROW) {
// -1 move left
ship.setDir(-1);
} // setir only when pressing key, want continuous movement
}
Please post a MCVE instead of a disconnected snippet that we can't run. Note that this should not be your entire project. It should be a small example sketch that just shows the problem without any extra code.
But to figure out what's going on, you need to debug your program. You need to find out stuff like this:
What is the length of every array? Are they continuously growing over time?
What is the actual framerate? Is the framerate dropping, or does it just appear to be slower?
At what point does it become slower? Try hard-coding different values to see what's going on.
Please note that I'm not asking you to tell me the answers to these questions. These are the questions you need to be asking yourself. (In fact, you should have all of these answers before you post a question on Stack Overflow!)
If you still can't figure it out, then please post a MCVE in a new question post and we'll go from there. Good luck.

Javascript challenge - which basket contains the last apple?

I'm presented with the following challenge question:
There are a circle of 100 baskets in a room; the baskets are numbered
in sequence from 1 to 100 and each basket contains one apple.
Eventually, the apple in basket 1 will be removed but the apple in
basket 2 will be skipped. Then the apple in basket 3 will be removed.
This will continue (moving around the circle, removing an apple from a
basket, skipping the next) until only one apple in a basket remains.
Write some code to determine in which basket the remaining apple is
in.
I concluded that basket 100 will contain the last apple and here's my code:
var allApples = [];
var apples = [];
var j = 0;
var max = 100;
var o ='';
while (j < max) {
o += ++j;
allApples.push(j);
}
var apples = allApples.filter(function(val) {
return 0 == val % 2;
});
while (apples.length > 1) {
for (i = 0; i < apples.length; i += 2) {
apples.splice(i, 1);
}
}
console.log(apples);
My question is: did I do this correctly? What concerns me is the description of "a circle" of baskets. I'm not sure this is relevant at all to how I code my solution. And would the basket in which the remaining apple reside be one that would otherwise be skipped?
I hope someone can let me know if I answered this correctly, answered it partially correct or my answer is entirely wrong. Thanks for the help.
So, ... I got WAY too into this question :)
I broke out the input/output of my last answer and that revealed a pretty simple pattern.
Basically, if the total number of items is a power of 2, then it will be the last item. An additional item after that will make the second item the last item. Each additional item after that will increase the last item by 2, until you reach another item count that is again divisible by a power of 2. Rinse and repeat.
Still not a one-liner, but will be much faster than my previous answer. This will not work for 1 item.
var items = 100;
function greatestPowDivisor(n, p) {
var i = 1;
while(n - Math.pow(p, i) > 0) {
i++;
}
return Math.pow(p, (i - 1));
}
var d = greatestPowDivisor(items, 2)
var last_item = (items - d) * 2;
I believe Colin DeClue is right that there is a single statement that will solve this pattern. I would be really interested to know that answer.
Here is my brute force solution. Instead of moving items ("apples") from their original container ("basket") into a discard pile, I am simply changing the container values from true or false to indicate that an item is no longer present.
var items = 100;
var containers = [];
// Just building the array of containers
for(i=0; i<items; i++) {
containers.push(true);
}
// count all containers with value of true
function countItemsLeft(containers) {
total = 0;
for(i=0; i<containers.length; i++) {
if(containers[i]) {
total++;
}
}
return total;
}
// what is the index of the first container
// with a value of true - hopefully there's only one
function getLastItem(containers) {
for(i=0; i<containers.length; i++) {
if(containers[i]) {
return(i);
}
}
// shouldn't get here if the while loop did it's job
return false;
}
var skip = false;
// loop through the items,
// setting every other to false,
// until there is only 1 left
while(countItemsLeft(containers) > 1) {
for(i=0; i<containers.length; i++) {
if(containers[i]) {
if(skip) {
skip = false;
} else {
containers[i] = false;
skip = true;
}
}
}
}
// what's the last item? add one to account for 0 index
// to get a human readable answer
var last_item = getLastItem(containers) + 1;
Needs error checking, etc... but it should get the job done assuming items is an integer.

Categories

Resources