I wrote a script to render a timeline with a dataset in javascript.
But i have to mark the area's where no info is found (yellow in the image).
I absolutely have no idea how to get the ranges without info in the dataset.
(i use momentjs for date calculations)
Example dataset:
data: [
{id: 1, lane: 1, start: 05-02-2006, end: 09-09-2008},
{id: 2, lane: 2, start: 01-01-2008, end: 31-07-2010},
{id: 3, lane: 3, start: 15-12-2013, end: 12-02-2016}
]
Example image:
I would create an new array and fill in the voids, something like this
var dates = [];
for (var i=0; i<data.length; i++) {
var now = data[i],
thisStart = parseDate(now.start),
thisEnd = parseDate(now.end),
prevEnd = data[i-1] ? parseDate(data[i-1].end) : null;
if ( prevEnd && prevEnd <= thisStart ) {
dates.push({id: i + 0.5, lane: 0, start: prevEnd, end: thisStart});
}
dates.push({id : now.id, lane: now.lane, start: thisStart, end: thisEnd});
}
and you'll end up with an array like this
dates : [
{id: 1, lane: 1, start: "2006-02-04T23:00:00.000Z", end: "2008-09-08T22:00:00.000Z"},
{id: 2, lane: 2, start: "2007-12-31T23:00:00.000Z", end: "2010-07-30T22:00:00.000Z"},
{id: 2.5, lane: 0, start: "2010-07-30T22:00:00.000Z", end: "2013-12-14T23:00:00.000Z"},
{id: 3, lane: 3, start: "2013-12-14T23:00:00.000Z", end: "2016-02-11T23:00:00.000Z"}
]
Note that I marked the object that fills the voids with an id if .5 and lane 0, you could do whatever you wanted there, as long as it's recognizable to you when you create the layout.
I also created a parseDate function, all it does is parse the dates for you to valid date objects, it's included in the fiddle below
FIDDLE
Related
I'm working on a calendar plugin that would show my occupation overview ordered as following:
Now I've got the event on the calendar with moment.js, so they are formatted like this (sorted on Start date):
let events = [{
name: 'Event A',
start: '01-11-2021 00:00',
end: '08-11-2021 00:00',
},{
name: 'Event C',
start: '03-11-2021 00:00',
end: '06-11-2021 00:00',
},{
name: 'Event E',
start: '05-11-2021 00:00',
end: '08-11-2021 00:00',
},{
name: 'Event D',
start: '07-11-2021 00:00',
end: '12-11-2021 00:00',
},{
name: 'Event B',
start: '10-11-2021 00:00',
end: '17-11-2021 00:00',
},]
Expected occupationOverview array would be something like:
let ooArray = [
{ // Longest/bottom bar
start: '01-11-2021 00:00',
end: '17-11-2021 00:00',
group: 1
},
{ // Middle bar 1
start: '03-11-2021 00:00',
end: '08-11-2021 00:00',
group: 2
},
{ // Middle bar 2
start: '10-11-2021 00:00',
end: '12-11-2021 00:00',
group: 2
},
{ // Top bar 1
start: '05-11-2021 00:00',
end: '06-11-2021 00:00',
group: 3
},
{ // Top bar 2
start: '07-11-2021 00:00',
end: '08-11-2021 00:00',
group: 3
},]
I have honestly no clue how to group the calendar events so they give back an array with the start and end times as resulted in the red box.
Anybody that can help me figure this out?
Thanks!
Based on the updated question, sort all the dates keeping the attribute for start and end. Process them in order so that the first date (that must be a start) starts level 1, which is single booked.
If the next date is an end, that ends the bar. However, if the next date is a start, that increases the level (i.e. double booked). The following is an implementation, you might want to sort the bars by level or start date.
The function below firstly gets all the dates sorted in ascending order with their type - start or end. It then processes each date - start dates create a new bar, end dates end the most recent bar. When ended, the last bar is popped off starts and added to bars, which is an array of finished bars.
This depends on the source data being valid, i.e. it must start with a start and end with an end, they must be in the right order and of equal number.
One enhancement would be to ensure that where a start and end have the same date, the start is always sorted before the end so zero length events (milestones?) don't get ordered end-start, which would cause the level to be decremented before it's incremented. There may be other issues with starts and ends that have the same date and time, please test.
// Parse date in D-M-Y H:m format
function parseDMY(s) {
let [D, M, Y, H, m] = s.split(/\D/);
return new Date(Y, M - 1, D, H, m);
}
// Format as DD-MM-YYYY HH:mm
function formatDate(d) {
let z = n => ('0'+n).slice(-2);
return `${z(d.getDate())}-${z(d.getMonth()+1)}-${d.getFullYear()} ` +
`${z(d.getHours())}:${z(d.getMinutes())}`;
}
// Generates "occupation" bars
function calcBookingLevels(events) {
// Get sorted array of [{type, Date}]
let dates = events.reduce( (dates, event) => {
dates.push({type: 'start', date: parseDMY(event.start)},
{type: 'end', date: parseDMY(event.end)});
return dates;
}, []).sort((a, b) => a.date - b.date);
// Process dates to get occupation bars with levels
let bars = [];
let starts = [];
let level = 0;
dates.forEach(date => {
// If it's a start, start a new bar
if (date.type == 'start') {
let bar = {level: ++level, start: formatDate(date.date)};
starts.push(bar);
// Otherwise it's an end, close the most recent bar and
// move to bars array
} else {
let t = starts.pop();
t.end = formatDate(date.date);
--level;
bars.push(t);
}
})
return bars;
}
// Sample data
let events = [{
name: 'Event A',
start: '01-11-2021 00:00',
end: '08-11-2021 00:00',
},{
name: 'Event C',
start: '03-11-2021 00:00',
end: '06-11-2021 00:00',
},{
name: 'Event E',
start: '05-11-2021 00:00',
end: '08-11-2021 00:00',
},{
name: 'Event D',
start: '07-11-2021 00:00',
end: '12-11-2021 00:00',
},{
name: 'Event B',
start: '10-11-2021 00:00',
end: '17-11-2021 00:00',
},];
// Run it...
console.log(calcBookingLevels(events));
If the dates were in YYYY-MM-DD HH:mm format then they could be sorted as strings without converting to Dates.
Looks like you need to get the diff in milliseconds, and sort from greatest to least. You'll have to play with this to see which way it sorts (might need to flip the 'a' and 'b' on the diff statement, as I didn't run it against your array)
const sortedArray = array.sort((a, b) => a.diff(b))
With the help of #RobG I figured it out. Here is my answer for interested people:
// Not needed for answer, but result gave warnings so yeah
moment.suppressDeprecationWarnings = true;
let newItems = []
let dateArray = []
let events = [{
name: 'Event A',
start: '01-11-2021 00:00',
end: '08-11-2021 00:00',
},{
name: 'Event C',
start: '03-11-2021 00:00',
end: '06-11-2021 00:00',
},{
name: 'Event E',
start: '05-11-2021 00:00',
end: '08-11-2021 00:00',
},{
name: 'Event D',
start: '07-11-2021 00:00',
end: '12-11-2021 00:00',
},{
name: 'Event B',
start: '10-11-2021 00:00',
end: '17-11-2021 00:00',
},]
// Make one big array with all start and end dates
events.forEach(event=> {
dateArray.push({
type: 'start',
date: event.start
}, {
type: 'end',
date: event.end
})
})
// Sort the dates from first to last
dateArray = dateArray.sort((left, right) => {
return moment(left.date).diff(moment(right.date))
})
let groupID = -1
// Loop through the array with all dates and add them to a group
// based on if the current date is a start or end
for (let i = 0; i < dateArray.length; i++) {
if (dateArray[i].type === 'start') groupID++
else groupID--
for (let ii = 0; ii <= groupID; ii++) {
if (dateArray[i + 1] &&
!moment(dateArray[i].date).isSame(dateArray[i + 1].date)) {
newItems.push({
group: 1,
start: dateArray[i].date,
end: dateArray[i + 1].date,
subgroup: 'sg_' + ii,
subgroupOrder: ii
})
}
}
}
console.log(newItems)
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
I'm working on a calendar app, which has an Agenda from "react-native-calendars". To display dates it needs the following:
items={{'2012-05-22': [{name: 'item 1 - any js object'}], ...}}
In my firebase backend I'm storing the start and end day of an appointment and also an array, which includes every day of it (so if the appointment started yesterday and ends tomorrow, the array stores yesterday, today and tomorrow as dates. The amount of dates in the array varies!)
The api data structure is the following:
apiData = [
{data: {allDays: ["2021-08-18", "2021-08-19", "2021-08-20"],
start: "2021-08-18",
end: "2021-08-20"
},
id: "string"},
{data: {allDays: ["2021-08-20", "2021-08-21", "2021-08-22", "2021-08-23"],
start: "2021-08-20",
end: "2021-08-23"
},
id: "string"},
//more Data
]
I got the following code to map the documents:
let markedDayAll = {};
{apiData.map(({data: {start, end, allDays}}) => (
markedDayAll[alldays] = [{
start: start,
end: end
}]
))};
which outputs in the correct "items" format, but right now, obviously, it is displayed like this:
items={{
'2021-08-18, 2021-08-19, 2021-08-20': [{start: "2021-08-18, end: "2021-08-20}]
}}
But I need the following:
items={{
'2021-08-18': [{start: "2021-08-18, end: "2021-08-20}],
'2021-08-19': [{start: "2021-08-18, end: "2021-08-20}],
'2021-08-20': [{start: "2021-08-18, end: "2021-08-20}]
}}
How do I properly map the data, so I get the desired result?
You can use array#reduce to iterate through each object and then iterate through each date in allDays and generate your object.
const apiData = [ {data: {allDays: ["2021-08-18", "2021-08-19", "2021-08-20"], start: "2021-08-18", end: "2021-08-20" }, id: "string"}, {data: {allDays: ["2021-08-20", "2021-08-21", "2021-08-22", "2021-08-23"], start: "2021-08-20", end: "2021-08-23" }, id:"string"} ],
result = apiData.reduce((r, {data}) => {
data.allDays.forEach(date => {
r[date] ??= [];
r[date].push({start: data.start, end: data.end});
});
return r;
},{});
console.log(result);
Consider I am having below array:
[{Id:'1' , Title: 'Group A' , Start: '100' , End:'200'} ,
{Id:'2' , Title: 'Group B' , Start: '350' , End:'500'} ,
{Id:'3' , Title: 'Group C' , Start: '600' , End:'800'} ]
I want to get unoccupied ranges between 100 and 999.
my required final result would be:
[{Start: '201' , End:'349'} ,
{Start: '501' , End:'599'} ,
{Start: '801' , End:'999'} ]
You need to iterate through each array item and find the prev unoccupied range and insert it in a new array. Then in the last item, you have to check if the last item occupies all space at the end up to maximum value. If not we need to add that range as well.
You can do the following using Array.reduce function,
let sortedArr = [{Id:'1' , Title: 'Group A' , Start: '100' , End:'200'} ,
{Id:'2' , Title: 'Group B' , Start: '350' , End:'500'} ,
{Id:'3' , Title: 'Group C' , Start: '600' , End:'800'} ];
const MIN = 100;
const MAX = 999;
let res = sortedArr.reduce((prev, curr, index, arr) => {
obj = {Start: 0, End: 0};
// checking if the first item has any unoccupied range before it's Start value.
if(index === 0 && Number(curr.Start) > MIN) {
obj = {Start: MIN, End: Number(curr.Start) - 1};
prev.push(obj);
} else if(index !== 0 && Number(arr[index - 1].End) + 1 < Number(curr.Start)) {
// If there is a range between the previous item's End and curr item's Start.
obj = {Start: Number(arr[index - 1].End) + 1, End: Number(curr.Start) -1};
prev.push(obj);
}
// checking if the last item has any range after it's End value.
if(index === arr.length -1 && Number(curr.End) < MAX) {
obj = {Start: Number(curr.End) + 1, End: MAX};
prev.push(obj);
} else {
return prev;
}
return prev;
}, [])
console.log(res);
Taking a back-to-basics approach: start with the full range as unoccupied, loop through each range in the data, if it starts inside an existing unoccupied range in the output (conversely, the first always will), cut that unoccupied range at the start of the data range and add a new unoccupied entry from the end of the data range to the end of the existing range that's being cut (actually clearer in code):
var range = [{ min: 100, max: 999 }];
var data = [
{Id:'1' , Title: 'Group A' , Start: 100 , End:200 },
{Id:'2' , Title: 'Group B' , Start: 350 , End:500 },
{Id:'3' , Title: 'Group C' , Start: 600 , End:800 }
];
for (let i=0; i<data.length; ++i) {
for (let r=0;r<range.length; ++r) {
// check if this data matches the existing data
// update existing data rather than create a new entry
if (range[r].min == data[i].Start) {
range[r].min = data[i].End+1;
break;
}
// check if this data sits inside the range
if (range[r].max > data[i].Start) {
// split the range, end one where data starts and add another where data ends
// (add the end part first before changing existing range[r].max)
range.push({ min: data[i].End+1, max: range[r].max})
range[r].max = data[i].Start-1;
break;
}
}
}
console.log(range);
If your data starts at the out-range start, then you'll need remove where max<min or put an explicit check in for this condition.
This uses the pre-requisites that the output data doesn't have any overlapping data (sort order doesn't matter, but output order will also not be sorted).
You can change min/max to Start/End in the output, kept them with different names for clarity.
I've also converted the data.Start/data.End to numeric, but you could add that inside the script if preferred (.Start*1 everywhere or your other preferred string-to-int conversion).
Here goes an example of how you can do it in javascript. You can use a for loop to iterate through the object and can create an row for each row in your input object by selecting End+1 from current row and Start-1 from next row. For the last row End will be 999.
output is your desired result.
var p = [
{Id:'1' , Title: 'Group A' , Start: '100' , End:'200'} ,
{Id:'2' , Title: 'Group B' , Start: '350' , End:'500'} ,
{Id:'3' , Title: 'Group C' , Start: '600' , End:'800'}
];
var i;
var output = [];
for (i = 0; i < p.length; i++) {
if (i + 1 < p.length)
output.push({ Start: Number(p[i].End) + 1, End: Number(p[i + 1].Start) - 1 });
else
output.push({ Start: Number(p[i].End) + 1, End: 999 });
}
console.log(output);
The console.log(output); you will get below output:
Array [Object { Start: 201, End: 349 }, Object { Start: 501, End: 599 }, Object { Start: 801, End: 999 }]
Here is another slightly more tolerant and generic approach that will allow any unsorted exclusion array (the exclusion ranges may even be overlapping):
const excl=[[400,450],[100,200],[350,500],[600,800],[150,250]],
sortedArr = [{Id:'1' , Title: 'Group A' , Start: '100' , End:'200'} ,
{Id:'2' , Title: 'Group B' , Start: '350' , End:'500'} ,
{Id:'3' , Title: 'Group C' , Start: '600' , End:'800'} ];
function allowedRanges(inc,excl){
var incl=[inc];
excl.forEach(x=>{
incl.forEach((r,i,a)=>{
if (x[0]<=r[1] && x[1]>=r[0])
a.splice(i,1,[r[0],x[0]-1],[x[1]+1,r[1]])
});
});
// now, remove the nonsensical ones
// and return the result:
return incl.filter(r=>r[1]>r[0]);
}
// test with an unsorted array excl:
console.log(allowedRanges([1,999],excl));
// test with sortedArr:
console.log(allowedRanges([101,999],sortedArr.map(e=>[+e.Start,+e.End])));
I've got an example array that I'm trying to reduce by the counts of the occurrence of a key (sentiment in this example):
const sentimentLog = [
{
id: 1,
createdOn: new Date('2020-02-13'),
sentiment: 1
},
{
id: 2,
createdOn: new Date('2020-02-12'),
sentiment: 1
},
{
id: 3,
createdOn: new Date('2020-02-12'),
sentiment: 2
},
{
id: 4,
createdOn: new Date('2020-02-11'),
sentiment: 3
},
{
id: 5,
createdOn: new Date('2020-02-11'),
sentiment: 2
},
{
id: 6,
createdOn: new Date('2020-02-10'),
sentiment: 1
},
{
id: 7,
createdOn: new Date('2020-02-10'),
sentiment: 2
},
{
id: 8,
createdOn: new Date('2020-02-09'),
sentiment: 1
}
]
I'm using:
const sentimentGrouped = (sentiments) => {
return sentiments.reduce((hash, { sentiment }) => {
hash[sentiment] = (hash[sentiment] || 0) + 1
return hash
}, [])
}
And it's nearly there. What I can't figure out is how to replace undefined when there's no sentiment scores of 0 (which is a possibility).
console.log('sentimentGrouped', sentimentGrouped(sentimentLog))
The above produces:
"sentimentGrouped" [undefined, 4, 3, 1]
Whereas I'd like:
"sentimentGrouped" [0, 4, 3, 1]
What am I missing?
Thanks in advance.
Edit: I'll elaborate a bit further, there's 4 scores that will be returned (0 to 3). The data returned will be based on a date range. So there may be instances where there'll be no 1s returned, similarly no 3s returned by a different date range.
The issue is that if you never touch an element of the array, then it stays as a hole in the array, which means it's treated as undefined. Since you know the length of the array i would just prefill the array with zeros. Any sentiment score that does occur will be incremented. Any one that doesn't will stay with its initial value.
return sentiments.reduce((hash, { sentiment }) => {
hash[sentiment] = hash[sentiment] + 1
return hash
}, [0, 0, 0, 0])
I am trying to read through an array of objects (that are start/end times) and combine two (or more) of those times if they are back to back.
ie. the first objects end time is the same as the next objects start time. If they are, combine them. Then check the newly combined objects end time with the next object in the array
Here is a simplified array of times:
var times = [
{
start: 1,
end: 2
},{
start: 2,
end: 3
},{
start: 4,
end: 5
},{
start: 6,
end: 7
},
]
I would like that (or have a diff Array) to output like the below:
var newTimes = [
{
start: 1,
end: 3
},{
start: 4,
end: 5
},{
start: 6,
end: 7
},
]
It gets trickier if there are 3 times in a row.
var threeTime = [
{
start: 1,
end: 2
},{
start: 2,
end: 3
},{
start: 3,
end: 5
},{
start: 6,
end: 7
},
]
The above should turn into:
var newThreeTimes = [
{
start: 1,
end: 5
},{
start: 6,
end: 7
},
]
The original array of times will always be sorted from oldest (smallest start time) to newest (largest start time). The output does not need to be in any specific order. All time objects will be moment times.
Can someone help me solve this?
This is the code i have come up with
function mergeArr(arr) {
// Sort the array in descending order
arr.sort(function(a, b) {
return b.start - a.start;
});
// Traverse from the top as you will need to remove the elements
// Merge the elements based on start of one and end of the previous
for (var i = arr.length - 1; i > 0; i--) {
if (arr[i].end == arr[i - 1].start) {
arr[i].end = arr[i - 1].end;
arr.splice(i - 1, 1);
}
}
// Sort it again in reverse order.
return arr.sort(function(a, b) {
return a.start - b.start;
});
}
Comments are making the code self explanatory.
Working Fiddle