I'm building an event scheduler and I've gotten into a point that I can't figure out a way to spread events around without overlapping each other. (It might have events at the same time and without limits. Whenever is possible, use 100% of the available width)
Here it is a picture of this scenario.
Some considerations:
The events is wrapped inside a div with position: relative and all the events has position:absolute.
Using javascript, I have to figure out what needs to be the value of top left width and height of each "div event" on the fly.
Events are an array of objects like the code below:
{
startAt: "12:00:30",
endsAt: "13:00:00",
description: "evt1",
id: '00001'
}
I'm using Vue.js to develop this project. But this is not an issue if you don't know Vue. I've build a small project using jsbin so you can just play around with a javascript function.
Live Code: https://jsbin.com/bipesoy/
Where I'm having problem?
I can't find a algorithm to calculate the top left width and height on the fly based on an array of events.
Some considerations about the jsbin code:
All the code to find the 4 properties above is inside the function parsedEvents
Inside parsedEvents you can access the array of events using: this.events
The job of parsedEvents is loop through the array of events and add the style propertie to each one and then return a new array of events with the style object.
Each 30 minutes has a height of 40px;
Any ideas how to accomplish it or a better solution?
After some time playing with this challenge I think it's time to give up from the idea. It is possible to program many possible scenarios of arranging events, but when you dig deep you realize it's very difficult to even write what exactly you want to be done and even if you manage, some of your decisions just don't look good on the screen. This is only with expanding events in width. Positioning and even re-positioning them to fill gaps is solved and not too difficult.
Snippet is here (or JSBin if you prefer: http://jsbin.com/humiyi/99/edit?html,js,console,output).
Green events are detected as "expandable". Grey o nes cannot expand. To clarify problems with logic of expanding, some examples:
evt 1 and evt3? it can be like this or evt3 goes right and they both expand
evt7 and evt12? Many ways to expand this... how to define a rule?
imagine evt7 and evt11 ar merged into one big event. How to expand evt10, evt7/11 and evt 12?
... now try to write rules to consistently answer on above 3 (and many many more possible scenarios not in this example)
My conclusion is that writing rules and developing this is not worth the effort. UI usability will not get much. They will even loose in some scenarios, i.e. some events would be visually bigger just because they have space and not because they're more important.
I'd suggest layout similar or exactly the same as the one in example. Events just don't expand. I don't know how much daily events you expect, what's real life scenario, but only upgrade I'd potentially do is to split vertically calendar in separated regions - point in time which is not overlapping with any event, like line between evt7 and evt11 in example. Then run this same script per region independently. That will recalculate vertical slots per region so region with evt10 i evt11 will have only 2 vertical slots filling space 50% each. This maybe be worth it if your calendar has few crowded hours and only a few events later/before. This would fix an issue of too narrow events later in the day without spending much time. But if events are all over the day and overlapping alot I don't think it's worth it.
let events = [
{ startAt: "00:00", endsAt: "01:00", description: "evt1", id: '00001' },
{ startAt: "01:30", endsAt: "08:00", description: "evt2", id: '00002' },
{ startAt: "01:30", endsAt: "04:00", description: "evt3", id: '00003' },
{ startAt: "00:30", endsAt: "02:30", description: "evt3", id: '00013' },
{ startAt: "00:00", endsAt: "01:00", description: "evt3", id: '00014' },
{ startAt: "03:00", endsAt: "06:00", description: "evt4", id: '00004' },
{ startAt: "01:30", endsAt: "04:30", description: "evt5", id: '00005' },
{ startAt: "01:30", endsAt: "07:00", description: "evt6", id: '00006' },
{ startAt: "06:30", endsAt: "09:00", description: "evt7", id: '00007' },
{ startAt: "04:30", endsAt: "06:00", description: "evt8", id: '00008' },
{ startAt: "05:00", endsAt: "06:00", description: "evt9", id: '00009' },
{ startAt: "09:00", endsAt: "10:00", description: "evt10", id: '00010' },
{ startAt: "09:00", endsAt: "10:30", description: "evt11", id: '00011' },
{ startAt: "07:00", endsAt: "08:00", description: "evt12", id: '00012' }
]
console.time()
// will store counts of events in each 30-min chunk
// each element represents 30 min chunk starting from midnight
// ... so indexOf * 30 minutes = start time
// it will also store references to events for each chunk
// each element format will be: { count: <int>, eventIds: <array_of_ids> }
let counter = []
// helper to convert time to counter index
time2index = (time) => {
let splitTime = time.split(":")
return parseInt(splitTime[0]) * 2 + parseInt(splitTime[1])/30
}
// loop through events and fill up counter with data
events.map(event => {
for (let i = time2index(event.startAt); i < time2index(event.endsAt); i++) {
if (counter[i] && counter[i].count) {
counter[i].count++
counter[i].eventIds.push(event.id)
} else {
counter[i] = { count: 1, eventIds: [event.id] }
}
}
})
//find chunk with most items. This will become number of slots (vertical spaces) for our calendar grid
let calSlots = Math.max( ...counter.filter(c=>c).map(c=>c.count) ) // filtering out undefined elements
console.log("number of calendar slots: " + calSlots)
// loop through events and add some more props to each:
// - overlaps: all overlapped events (by ref)
// - maxOverlapsInChunk: number of overlapped events in the most crowded chunk
// (1/this is maximum number of slots event can occupy)
// - pos: position of event from left (in which slot it starts)
// - expandable: if maxOverlapsInChunk = calSlot, this event is not expandable for sure
events.map(event => {
let overlappedEvents = events.filter(comp => {
return !(comp.endsAt <= event.startAt || comp.startAt >= event.endsAt || comp.id === event.id)
})
event.overlaps = overlappedEvents //stores overlapped events by reference!
event.maxOverlapsInChunk = Math.max( ...counter.filter(c=>c).map(c=>c.eventIds.indexOf(event.id) > -1 ? c.count : 0))
event.expandable = event.maxOverlapsInChunk !== calSlots
event.pos = Math.max( ...counter.filter(c=>c).map( c => {
let p = c.eventIds.indexOf(event.id)
return p > -1 ? p+1 : 1
}))
})
// loop to move events leftmost possible and fill gaps if any
// some expandable events will stop being expandable if they fit gap perfectly - we will recheck those later
events.map(event => {
if (event.pos > 1) {
//find positions of overlapped events on the left side
let vertSlotsTakenLeft = event.overlaps.reduce((result, cur) => {
if (result.indexOf(cur.pos) < 0 && cur.pos < event.pos) result.push(cur.pos)
return result
}, [])
// check if empty space on the left
for (i = 1; i < event.pos; i++) {
if (vertSlotsTakenLeft.indexOf(i) < 0) {
event.pos = i
console.log("moving " + event.description + " left to pos " + i)
break
}
}
}
})
// fix moved events if they became non-expandable because of moving
events.filter(event=>event.expandable).map(event => {
let leftFixed = event.overlaps.filter(comp => {
return event.pos - 1 === comp.pos && comp.maxOverlapsInChunk === calSlots
})
let rightFixed = event.overlaps.filter(comp => {
return event.pos + 1 === comp.pos && comp.maxOverlapsInChunk === calSlots
})
event.expandable = (!leftFixed.length || !rightFixed.length)
})
//settings for calendar (positioning events)
let calendar = {width: 300, chunkHeight: 30}
// one more loop through events to calculate top, left, width and height
events.map(event => {
event.top = time2index(event.startAt) * calendar.chunkHeight
event.height = time2index(event.endsAt) * calendar.chunkHeight - event.top
//event.width = 1/event.maxOverlapsInChunk * calendar.width
event.width = calendar.width/calSlots // TODO: temporary width is 1 slot
event.left = (event.pos - 1) * calendar.width/calSlots
})
console.timeEnd()
// TEST drawing divs
events.map(event => {
$("body").append(`<div style="position: absolute;
top: ${event.top}px;
left: ${event.left}px;
width: ${event.width}px;
height: ${event.height}px;
background-color: ${event.expandable ? "green" : "grey"};
border: solid black 1px;
">${event.description}</div>`)
})
//console.log(events)
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
</body>
</html>
was thinking more about this. I covered width, height, and top. But you might have issues with calculating left. So a little change... goal to create counters for each 30-min increment still remains, but instead of looping through increments and filtering items, do it vice versa. Loop through items and fill that counter object from there. It will also be faster. While on that loop, don't just store counter to counter object, but also push itemID to another property of the same object (and the same increment). So, each increment can be represented like {span: "17:00", counter: 2, items: [135, 148]}. This items array will later help you to determine left position of each item and avoid horizontal overlapping. Because maximal poistion of the item in items array across all increments of the item = position of the item on the calendar from left. Multiply it with width (-1) and you have left.
Related
I want to be able to get range slider and a selector in my graph, I have followed the example in the documentation, but I’m getting the following error:
1.- The selector dates, are still using ‘backwards’, as opposed to ‘todate’, which is a bit weird, perhaps is the fact that I'm not understanding this 100%, but I would like to get 6 and 12 months from today, is there a way to use a forward from the earliest date period?
https://jsfiddle.net/jt1o26bd/
var Deals = {
x: {{ deals_plot.lic_deals_date|safe }},
y: {{ deals_plot.lic_deals_licenses }},
name: 'Active Licenses',
type: 'bar',
marker: {
color: 'rgb(0,131,117)',
}
};
var Leads = {
x: {{ deals_plot.lic_leads_date|safe }},
y: {{ deals_plot.lic_leads_licenses }},
name: 'Potential Licenses',
type: 'bar',
marker: {
color: 'rgb(160,220,210)',
}
};
var data = [Deals,Leads];
var layout = {
title: 'Software Licenses Term',
barmode: 'stack',
xaxis: {
autorange: true,
rangeselector: {buttons: [
{step: 'all'},
{
count: 1,
label: 'YTD',
step: 'year',
stepmode: 'todate'
},
{
count: 6,
label: '6m',
step: 'month',
stepmode: 'todate'
}
]},
rangeslider: { },
type: 'date',
tickfont:{
size: 14
},
},
yaxis: {
tickfont:{
size: 14
}
}
};
Could anyone let me know what is going on?
You might already understand the difference between backward and todate but backward will go back by exactly the count number of steps, and todate will go back by whatever amount of time is needed to round to the first count number of steps.
For example, the expected behavior of the YTD button constructed with arguments count: 1, step: 'y', stepmode: 'todate' will be to go back to Jan 1 of from the year at the end of your current daterange. Your jsfiddle does indeed do this:
If you were to construct your YTD button with stepmode: 'backward', then when you click on the button, the daterange would instead move back to Nov 2022.
Since you asked about buttons going forward in time instead of backward, this feature does not currently exist in plotly according to this forum post, but #scoutlt appears to have solved the problem for themselves by modifying their local version of plotly.js - however their solution is no longer up to date with the latest version of the library.
If you go into plotly.js/src/components/rangeselector/get_update_object.js you can see that there is a function called getXRange where the backward and todate are defined:
function getXRange(axisLayout, buttonLayout) {
var currentRange = axisLayout.range;
var base = new Date(axisLayout.r2l(currentRange[1]));
var step = buttonLayout.step;
var utcStep = d3Time['utc' + titleCase(step)];
var count = buttonLayout.count;
var range0;
switch(buttonLayout.stepmode) {
case 'backward':
range0 = axisLayout.l2r(+utcStep.offset(base, -count));
break;
case 'todate':
var base2 = utcStep.offset(base, -count);
range0 = axisLayout.l2r(+utcStep.ceil(base2));
break;
}
var range1 = currentRange[1];
return [range0, range1];
}
Personally, I think the best solution would be to define another case called forward which does the opposite of backward:
switch(buttonLayout.stepmode) {
case 'backward':
range0 = axisLayout.l2r(+utcStep.offset(base, -count));
break;
case 'forward':
range0 = axisLayout.l2r(+utcStep.offset(base, count));
break;
case 'todate':
var base2 = utcStep.offset(base, -count);
range0 = axisLayout.l2r(+utcStep.ceil(base2));
break;
}
However, I haven't worked much with the plotly.js repo, and there is the possibility that this will cause a breaking change (and might break one or more of the unit tests, for example).
If that is the case, you can do something similar to what #scoutlt did in the forum post, and change the definition of backward to mean forward (personally i think this is a pretty hacky approach, so I would only do this if defining forward doesn't work, and you want your buttons to ONLY go forward). That would look something like:
switch(buttonLayout.stepmode) {
case 'backward':
range0 = axisLayout.l2r(+utcStep.offset(base, count));
break;
case 'todate':
var base2 = utcStep.offset(base, -count);
range0 = axisLayout.l2r(+utcStep.ceil(base2));
break;
}
I'm using vis-timeline to render a hundred rows of data. When the component loads, I want to see the rows starting from the beginning: 1, 2, 3, and so on. Instead, by default, vis-timeline starts by displaying the end of the list (...97, 98, 99, 100), so that you have to scroll up to get to the top?
There are methods like setWindow() for setting the horizontal time frame, but what about the vertical position? I've tried the HTML scroll() method, but that doesn't seem to do anything.
Normally I would use orientation: { item: 'top' } } in the options, but there is a bug that prevents scrolling in that case; it must be set to 'bottom'.
I've thought about initializing the component with the orientation to 'top', then once it displays, setting it to 'bottom' to allow scrolling, but that seems pretty hacky.
Is there a cleaner solution?
You can implement a custom ordering function for items (groups as well) using the order configuration option:
Provide a custom sort function to order the items. The order of the
items is determining the way they are stacked. The function order is
called with two arguments containing the data of two items to be
compared.
WARNING: Use with caution. Custom ordering is not suitable for large
amounts of items. On load, the Timeline will render all items once to
determine their width and height. Keep the number of items in this
configuration limited to a maximum of a few hundred items.
See below for an example:
var items = new vis.DataSet();
for (let i=0; i<100; i++) {
items.add({
id: i,
content: 'item ' + i,
start: new Date(2022, 4, 1),
end: new Date(2022, 4, 14)
});
}
var container = document.getElementById('visualization1');
var options = {
order: function (a, b) {
return b.id - a.id;
}
}
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setItems(items);
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis-timeline-graph2d.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js" rel="script"></script>
<div id="visualization1"></div>
When making a level in kaboomJS with a large tile map collisions, things start to get slow... So I was wondering if there was an easy way to merge multiple tiles like maybe a whole row of blocks could be treated as one large block?
1. Tiles don't have to fit in the grid
If you want to reduce the number of Game Objects in the Scene at a time you can have a single symbol in your level definition represent a Game Object that spans multiple grid tiles. So if you want a lot of platforms that are 3 grid squares wide, you don't need 3 objects per platform, you can just use a single character to represent a 3x1 rect:
import kaboom from "kaboom"
// initialize context
kaboom()
// load assets
loadSprite("bean", "sprites/bean.png")
addLevel(
// Note: the hyphens here hare just place holders to remind us that the
// game objects created by ➁ and ➂ are actually taking up 2 and 3 grid
// squares respectively.
[
" ⚥ ",
" ",
" ➂-- ",
" ",
" ➁- ",
" ",
" ",
"################################",
],
{
width: 32,
height: 32,
"⚥": () => [
sprite("bean"),
area(),
body(),
"player"
],
"#": () => [
rect(32, 32),
outline(2),
area(),
solid(),
],
"➁": () => [
rect(32 * 2, 32),
outline(2),
area(),
solid(),
],
"➂": () => [
rect(32 * 3, 32),
outline(2),
area(),
solid(),
],
}
);
const player = get("player")[0];
player.onUpdate(() => {
const left = isKeyDown('left') || isKeyDown('a');
const right = isKeyDown('right') || isKeyDown('d');
if (left && !right) {
player.move(-500, 0);
} else if (right && !left) {
player.move(500, 0);
}
camPos(player.pos);
});
onKeyPress("space", () => {
if (player.isGrounded()) {
player.jump();
}
});
Obviously if you had many different shapes and sizes this would be quite onerous.
2. Advanced: Quad Trees and Loading/Unloading Regions
I actually ran into this problem myself on a recent Kaboom project and decided to completely overhaul the built in addLevel() with my own implementation that loaded from a bitmap instead of a bunch of strings, and then organized the level data into a quadtree so that I could quickly find chunks that overlapped the visible area, and load and unload game objects based on their visibility. The technique and code are a bit to complex it include here, so I'll just link to the Repl and the relevant source code: level-loader.ts and lib-quad-tree.ts and the usage in level-one.js .
In the highcharts example above suppose I have 100 series in Bananas which is 1 right now and just one series in Apples ,and if there is a lot of empty space between Bananas and Oranges can we reduce the spacing between them ?
The reason is if there are 100 series in Bananas due to space constraint every line gets overlapped even though there is extra space available between Bananas and Apples . Also is it possible to remove "Oranges" if it doesnt have any series at all and accomodate only series from "Bananas"?
Categories functionality works only for constant tick interval equaled to 1. What you're trying to achieve is having a different space reserved for every category. That means that tick interval has to be irregular.
Unfortunately Highcharts doesn't provide a property to do that automatically - some coding and restructuring the data is required:
All the points have specified x position (integer value)
xAxis.grouping is disabled and xAxis.pointRangeis 1
Following code is used to define and position the labels:
events: {
render: function() {
var xAxis = this.xAxis[0];
for (var i = 0; i < xAxis.tickPositions.length; i++) {
var tickPosition = xAxis.tickPositions[i],
tick = xAxis.ticks[tickPosition],
nextTickPosition,
nextTick;
if (!tick.isLast) {
nextTickPosition = xAxis.tickPositions[i + 1];
nextTick = xAxis.ticks[nextTickPosition];
tick.label.attr({
y: (new Number(tick.mark.d.split(' ')[2]) + new Number(nextTick.mark.d.split(' ')[2])) / 2 + 3
});
}
}
}
}
(...)
xAxis: {
tickPositions: [-0.5, 6.5, 7.5],
showLastLabel: false,
labels: {
formatter: function() {
switch (this.pos) {
case -0.5:
return 'Bananas';
case 6.5:
return 'Apples';
}
}
}
}
Live demo: http://jsfiddle.net/BlackLabel/2Lcs5up5/
I have some json along the lines of
{
"user1": [{"distance": 1, "pace": 100 }, {"distance": 4, "pace": 120}, {"distance": 9, "pace": 110}],
"user2": [{"distance": 1, "pace": 110 }, {"distance": 7, "pace": 130}, {"distance": 14, "pace": 140}],
}
In total there are up to 30 users, with each user having up to 500 'frames'.
I want to animate an icon (svg or png or css) for each user simultaneously so that all 30 users can go across the screen.
How can I do this, ideally in a way that would play nicely with react.js as I will probably be using that to handle other things on the screen.
Alternatively, can I do it purely in react.js and with natively javascript?
Basically you need to store the progress (e.g. milliseconds since start) in state, and have each person component figure out its position based on the array and that number. You can just do something like this:
componentDidMount: function(){
var start = Date.now();
var tick = function(){
this.setState({time: Date.now() - start});
};
setInterval(tick.bind(this), 16);
},
render: function(){
var people = this.props.people;
return <div>{
Object.keys(people).map(function(key){
return <Person data={people[key]} time={this.state.time} key={key} />
}.bind(this));
}</div>
}