If elements are touching, split their width in half? - javascript

I'm working on a Javascript/jQuery calendar which includes a month view and a day view. Clicking the days will change the date, which will update the date variables in the day view.
The day view is split up into half hour segments from midnight to 11:00 PM. Clicking on any half hour <tr> (the day view is a table) will create an event between that time clicked and an hour in the future, as well as append a div on top of the calendar, spanning the range of time and positioned at the correct starting point (each pixel is a minute...)
There is a problem, however. If you create an "event" between a certain time span where there is already one in place, they overlap. This is the default behavior, obviously, but what I would like to happen is that if an event is created between a range of dates that is already occupied by an event, they align side by side so that they're not overlapping.
This resembles the behavior seen in the iCal app for mac:
Now my first thought to achieve such a goal was to use collision detection, but all the jQuery plugins for this are bloated or require the elements to be draggable.
Then I thought there might be a way in CSS to do this, where if two elements are overlapping, they split the width evenly.
Then I thought that's ridiculously far fetched, so I'm wondering how I can achieve this as easily as possible.
I'll post the full code in a jsFiddle, but for the most important function would be insertEvent which looks like this:
function insertEvent(start, end){
var end_minutes = new Date(end).getMinutes();
var end_border = new Date(new Date(end).setMinutes(end_minutes + 2));
//$(".day_date").html(start + "<br />" + end);
var diff = Math.abs(end_border - new Date(start));
var minutes = Math.floor((diff/1000)/60);
var start_element = $("td").find("[data-date='" + start + "']");
var offset = start_element.offset().top - $(".second").offset().top;
var this_element = $("<div class='event' style='height:" + minutes + "px;margin-top:" + offset + "px;'></div>");
$(".right").prepend(this_element);
}
This takes two parameters in the javascript new Date() format, one for the start date and one for the end date.
The fiddle: http://jsfiddle.net/charlescarver/HwdwL/

One of the the problems I see with your approach is that there isn't a structure to the storage of the data. I've built a calendar in Javascript before and it's not easy work. First, make sure you have some kind of abstraction for the calendar event. Something like:
function CalendarEvent(startDateTime, endDateTime) {
this.startDateTime = startDateTime;
this.endDateTime = endDateTime;
}
CalendarEvent.prototype.start = function() {
return this.startDateTime.getTime();
};
CalendarEvent.prototype.end = function() {
return this.endDateTime.getTime();
};
CalendarEvent.new = function(startDateTime, endDateTime) {
// This is a little factory method. It prevents calendar events
// from having end times that fall before the start time.
// USE THIS TO INSTANTIATE A NEW CALENDAR EVENT
if(endDateTime.getTime() < startDateTime.getTime()) {
throw new Error("End time falls before start time");
}
return new CalendarEvent(startDateTime, endDateTime);
};
CalendarEvent.compare = function(eventOne, eventTwo) {
// this is a class method to compare two events
// If used with sort it will sort by startDateTime
return eventOne.start() - eventTwo.start();
};
// ... add any other methods you need
Next you're going to want to sort the calendar events. I would sort by start time. Then once it is sorted you can actually re-render everything when changes are made. As long as you sort correctly, determining if a calendar event collides is as simple as this:
CalendarEvent.prototype.intersects = function(otherEvent) {
// If the other event starts after this one ends
// then they don't intersect
if(otherEvent.start() > this.end()) {
return false;
}
// If the other event ends before this one starts
// then they don't intersect
if(otherEvent.end() < this.start()) {
return false;
}
// Everything else is true
return true;
};
Because the data is sorted you know that if two or more calendar events intersect they will have to share the space. Granted, you must think about a few things when you divide the space. Do you want a naive implementation where you just share the space equally from left to right (left having the earliest start time). If so your visual representation could look like this if it had 4 events that shared a space (each block is an event):
However if your events have strange shapes they might cause your calendar to look strange. Consider the following:
In this instance event 2 takes up a lot of vertical space and all the space underneath event 1 is unused. Maybe for a better UX you don't want that kind of thing to happen. If so you should design your rendering algorithm accordingly. Just remember that it is probably easiest to re-render on every change that you encounter, but it's all about how you store the data. If you do not store the data in some kind of structure that is easily traversed then you won't be able to do this kind of thing.
But to complete the answer to your question, here is a fairly naive example. I haven't tested it so this is a pretty big assumption of it working. It is not entirely complete you will have to edit the rendering for yourself. This is merely to give you an idea of how to get it to work. It could definitely look prettier:
function renderCalendarEvents(calendarEvents) {
// Sort the calendar events (assuming calendarEvents is an array)
var sortedEvents = calendarEvents.sort(CalendarEvent.compare);
var index = 0;
// renderEvents is an anonymous function that will be called every time
// you need to render an event
// it returns it's columnDivisor.
var renderEvent = function(position) {
var currentEvent = sortedEvents[index];
var nextEvent = sortedEvents[index + 1];
// The default column divisor is determined by
// the current x-position + 1
var columnDivisor = position + 1;
// Increment before any recursion
index += 1;
// Check if nextEvent even exists
if(nextEvent) {
// If the nextEvent intersects with the current event
// then recurse
if(currentEvent.intersects(nextEvent)) {
// We need to tell the next event that it starts at the
// column position that is immediately +1 to the current event
columnDivisor = renderEvent(position + 1);
}
}
// placeEvent() is some function you can call to actually place
// the calendar event element on the page
// The position is the x-position of the current event
// The columnDivisor is a count of the amount of events sharing this column
placeEvent(currentEvent, position, columnDivisor);
return columnDivisor;
};
while(true) {
// render events until we're done
renderEvent(0);
if(index >= sortedEvents.length) {
break;
}
}
}
Essentially the idea with this particular algorithm is that if the nextEvent on the list exists and that event intersects with the currentEvent then we need to split the width of the currentEvent. It keeps on recursing until it finds no more intersections then it makes it's way back up the chain of recursive calls. I skipped the actual DOM manipulation logic because really the hard part is determining how much you need to split the actual column in order to get these events to fit. So hopefully this all makes a little bit of sense.
EDIT:
To be much more clear, in order to add this to your existing code I would replace your insertEvent function with something like this. I don't write all of the logic for you so you'll have to do some of your own writing. But that's half the fun :-).
function insertEvent(start, end) {
var newEvent = Calendar.new(start, end);
// you'll have to store the array somewhere.
// i'm just assuming some kind of global right now
eventsArray.push(newEvent);
// You'll want to destroy any event elements
destroyCurrentEventElements();
// Now run the rendering function
renderCalendarEvents(eventsArray);
}

Related

dc.js line chart starting from zero after each point

I have a line chart as shown in the fiddle http://jsfiddle.net/djmartin_umich/qBr7y/
The graph works fine and plots as expected. But I need one change to be made so that the plots become triangular shaped and I could see a series of irregular triangles. I mean after every point in Y, it should drop to 0 and start afresh. I know we could achieve this by explicitly adding data points to point to 0. But, just wondering if we could do that without creating additional data points.
HTML:
<div id="line-chart"></div>
<div id="log">Incoming Data:</div>
JS:
var startDate = new Date("2011-11-14T16:17:54Z");
var currDate = moment(startDate);
var cf = crossfilter([{date: startDate, quantity: 1}]);
AddData();
var timeDimension = cf.dimension(function(d){ return d.date; });
var totalGroup = timeDimension.group().reduceSum(function(d){ return d.quantity; });
var lineChart = dc.lineChart("#line-chart")
.brushOn(false)
.width(800)
.height(200)
.elasticY(true)
.x(d3.time.scale().domain([startDate, currDate]))
.dimension(timeDimension)
.group(totalGroup);
dc.renderAll();
window.setInterval(function(){
AddData();
lineChart.x(d3.time.scale().domain([startDate, currDate]));
dc.renderAll();
}, 800);
function AddData(){
var q = Math.floor(Math.random() * 6) + 1;
currDate = currDate.add('day', 5);
cf.add( [{date: currDate.clone().toDate(), quantity: q}]);
$("#log").append(q + ", ");
}
CSS:
#log{
clear:both;
}
Thanks,
Vicky
You can use a "fake group" to achieve this effect.
This is a general-purpose technique for preprocessing data that allows you to change what the chart sees without modifying the data in the crossfilter. In this case, we want to add a data point immediately after each point returned by the crossfilter group.
The fake group wraps the crossfilter group in an object that works like a group. Since in most cases dc.js only needs to call group.all(), this is pretty easy:
function drop_to_zero_group(key_incrementor, group) {
return {
all: function() {
var _all = group.all(), result = [];
_all.forEach(function(kv) {
result.push(kv);
result.push({key: key_incrementor(kv.key), value: 0});
})
return result;
}
}
}
The fake group here produces two data points for each one it reads. The first is just a duplicate (or reference) of the original, and the second has its key incremented by a user-specified function.
It might make sense to parameterize this function by the zero value as well, but I mostly wanted to pull out the date incrementor, since that involves another trick. Here is a date incrementor:
function increment_date(date) {
return new Date(date.getTime()+1);
}
This uses date.getTime() to get the integer value (in milliseconds since the beginning of 1970), adds one, and converts back to a date.
Actually, the first time I tried this, I forgot to include +1, and it still worked! But I don't recommend that, since dc.js is likely to get confused if there is more than one point with the same x value.
Apply the fake group by wrapping the group before passing it to the chart
lineChart
.group(drop_to_zero_group(increment_date, totalGroup));
Here's a fork of DJ's fiddle: http://jsfiddle.net/gordonwoodhull/dwfgma8j/4/
FWIW I also changed dc.renderAll() to dc.redrawAll() in order to enable animated transitions instead of blinking white and rendering from scratch each time. The transitions are not perfect but I think it's still better than the blink. I have a fix but it's a breaking change so it will go into dc.js 2.1.

Understanding groupJoin

I do not fully understand this operator.
https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/groupjoin.md
How to understand those parts:
Correlates the elements of two sequences based on overlapping durations, and groups the results.
leftDurationSelector (Function): A function to select the duration (expressed as an observable sequence) of each element of the left observable sequence, used to determine overlap.
rightDurationSelector (Function): A function to select the duration (expressed as an observable sequence) of each element of the right observable sequence, used to determine overlap.
From the 4th argument:
(Observable) An observable sequence with elements from the right sequence that overlap with the left sequence's element.
Why here 2nd and 3rd arguments are like this? What does timer(0) do good for us? I understand that timer(0) after 0 seconds returns value 0 and completes. So what?
var xs = Rx.Observable.interval(100)
.map(function (x) { return 'first' + x; });
var ys = Rx.Observable.interval(100)
.map(function (x) { return 'second' + x; });
var source = xs.groupJoin(
ys,
function () { return Rx.Observable.timer(0); },
function () { return Rx.Observable.timer(0); },
function (x, yy) {
return yy.select(function (y) {
return x + y;
})
}).mergeAll().take(5);
var subscription = source.subscribe(
function (x) {
console.log('Next: ' + x);
},
function (err) {
console.log('Error: ' + err);
},
function () {
console.log('Completed');
});
Maybe you know better source of explaination?
Here are sources that you should review for a better understanding of groupJoin. By curiosity what do you aim to achieve by using this operator? There might be alternative ways to do the same. I have yet to come with an practical interesting use of groupJoin.
illustrated marble
unit tests (i.e. specifications, look speciialy the first one GroupJoinOp_Normal_I)
examples of usage
I recommend you review these resources in order (and start by understanding join), and spend some time on the marble to understand what's going on.
About the official documentation example, timer(0) basically gives you duration selectors of one tick. The duration selector serves only as signals, their values is irrelevant to the output. When the duration selector emits a value or terminates, stuff happens (emission of grouped values from both streams). While they don't emit a value neither terminate, stuff happens too (grouping of values from both streams).
The selector function you pass receives the grouped values and computes other values from it. With groupJoin, that selector receives the value from the first source, and an observable which groups all the relevant values from the second source, and you do whatever logic you want with that.
After user3743222 posted in answer, I took more time to understand from that page, I constantly check reactivex, but for this function it was hard to understand those arrows going from left to right with bit of angle.
There was mentioned windows. And it turns out that those arrows represent windows.
Second and third parameter is the window width:
function () { return Rx.Observable.timer(500); }
For example above window width is 500 milliseconds (assuming that in this case we can measure width in time units).
I painted a graphic which looks for me more like windows:
So from my graph we can see for example that black window intersects with green window and so the events are joined by function in 4th parameter.
Same with green and red.
I did not investigate other cases, like what happens if more than one event from first stream windows intersect with 2nd stream, but at least we have an idea how it works, and maybe someone else will add info if there is some unexpected cases.
Also needs further investigation window widths of 0, because visually looking they do not exist, I mean if there is a window, I imagine it having width > 0.

Fluent animation to follow the cursor

I trying to figure out, how to create a smooth cursor follow animation with cocos2d js
Here some code:
var listener = cc.EventListener.create({
event: cc.EventListener.MOUSE,
onMouseMove: function (event) {
var str = "MousePosition X: " + event.getLocationX() + " Y:" + event.getLocationY();
var actionBy = cc.MoveTo.create(2, cc.p(event.getLocationX(), $scope.bar._position.y));
$scope.bar.stopAllActions();
$scope.bar.runAction(
cc.sequence(
//cc.rotateTo(2, 0),
actionBy
)
);
// do something...
}});
The problem here, its stuck because the event is fired to often and the "$scope.bar.stopAllActions();" stops the animation.
If I remove the "$scope.bar.stopAllActions();" the animation is driving crazy, the "bar" is flying over the screen.
I just wont the bar follow the mouse cursor, just like the dog follow human
The problem here is that if you remove stopAllActions(); you'd be trying to run an action on an object that's already got an action of the same time. And in either case you are firing an animation too often on an object, I've rarely seen this work as expected.
I'd try running the action directly instead of putting it within a cc.Sequence first, but if that doesn't work, you've got two choices, both rely on manual labor:
a. Simply put the bar on the position of the mouse whenever it moves:
$scope.bar.x = event.getLocationX();
b. Assuming you want to "juice up" the game, just setting the bar on the mouse will be boring, so you could make the bar progressively catch up with the mouse:
In your constant section:
var EASING_CONSTANT = 0.9;
In the event handler:
$scope.bar.x += ($scope.bar.x - event.getLocationX()) * EASING_CONSTANT;
The lower your EASING_CONSTANT the slower the bar'll catch up to the mouse (only use values between 0 and 1).
Now, if you try to use this, you'll realize the bar never quite catches up with your mouse, so you'll have to place this code within an update function (in your main game layer's update method, for example) so it runs each frame.
BUT! Then you won't have access to an event object, so you'll have to end up with something like this:
In the event handler:
$scope.bar.targetX = event.getLocationX();
In the update method:
$scope.bar.x += ($scope.bar.x - $scope.bar.targetX) * EASING_CONSTANT;
There are lots of easing functions you can use, I just gave you the simplest example.
For instance, note that you also have the event.getDeltaX() method that'll return you the difference in position between the last call of the event and the current (how much the mouse moved since the last call of the event). With this you could do something along the lines of:
In your constant section:
var EASING_CONSTANT = 0.9;
var WOBBLING_CONSTANT = 10;
In the event handler:
$scope.bar.targetX = event.getLocationX();
$scope.bar.mouseDeltaX = event.getDeltaX();
In the update method:
$scope.bar.x += ($scope.bar.x - $scope.bar.targetX) * EASING_CONSTANT + Math.cos($scope.bar.mouseDeltaX) * WOBBLING_CONSTANT;
Just get creative and start messing around with the formula until you find a behaviour that "feels" right. But be sure to start simple first!
PS: I'm guessing you want your "bar" to only move on the x-axis, so.. are you making an arkanoid clone? :D

Script to measure the passage of time?

I'm currently developing a small game for a class using Crafty, and our game requires the passage of time. To give context, it's a tamagotchi-like game where the player controls a creature and feeds it, grooms it, etc., when required, so we need to measure time for things such as "every five minutes the creature will be hungry" or "after a certain amount of time (say, 20 minutes) the creature will die of natural causes and the game will end". My problem is that I'm not sure what is the best way to go about this. Should I use setInterval? setTimeout? My own timer class? I don't know what's the most common solution for instances like these. So far I've tried the first two options without successful results.
I also tried looking for any crafy functions for time; and the closest I could find was
Crafty.bind("EnterFrame", function() { ... }), so I could 'manipulate' time frame by frame, but this didn't work either. Thanks in advance.
When the game starts, get a timestamp with new Date().getTime(), and keep it in a variable. On regular intervals (you can use setInterval), compare that with the current timestamp, and act according to how much time has passed.
I suggest building an array with the timed events, and checking its elements from the timer callback. Something like this:
var startTime = new Date().getTime();
// List of timed events
// (times are in milliseconds)
var events = [
{ time: 5000, text: "do something after 5 secs"},
{ time: 10000, text: "do something after 10 secs"},
{ time: 20000, text: "do something after 20 secs"}
];
// Check for events every second
setInterval(function() {
var timePassed = new Date().getTime() - startTime;
console.log("passed: " + timePassed + " milliseconds");
// Check all events
for(var i=events.length-1; i>=0; i--) {
// If an event has expired, remove it from the list,
// (and do whatever tou need to do)
if(events[i].time <= timePassed) {
// log the event text to the console
console.log(timePassed + "ms passed. " + events[i].text);
// remove from array
events.splice(i, 1);
}
}
}, 1000);
DEMO (open your browser console to see the output).
Rather than use the single timestamp as in bfavartto's answer, you could expand that to set multiple, dynamic timestamps in a "time" array (or object) or even simply as individual variables and then update them as actions occur.
A simple version would be something like:
// Initialize the variables at the beginning of the game
var lifeStamp = new Date().getTime();
var foodStamp = lifeStamp ;
var groomStamp = lifeStamp ;
function feed() {
/*** Do feed-realted stuff here ***/
// reset the "feed" timestamp
foodStamp = new Date().getTime();
}
function groom() {
/*** Do groom-realted stuff ***/
// reset the "groom" timestamp
groomStamp = new Date().getTime();
}
That way, you could keep your intervals, but reset them by resetting the timestamps that your intervals compare against for different types of actions. At the same time, the "life" timestamp would stay the same, while the other ones changed.
Edit: Re-looking at bfavartto's answer, it looks like you would need to store off and "original" list of any action-specific events as well, so that, as you reset the timestamps, you could reset the actions that go with them.
Edit #2: One more thing . . . if you want these creatures to last past the current page load, you might want to look into using cookies or HTML5's LocalStorage to store these timestamps. That would allow you to (practically) use much longer intervals, as well as let users leave the page and come back to pick up where they left off (though, I'd imagine that there would be a considerable amount of other data that would need to be stored as well, in order to allow users to leave and come back).

Multiple simultanous animations, simulation of 'animate' easing and one 'step' call for a jQuery collection

Here is the problem, I've got a tree structure of html blocks, global container is of a fixed width(X) and height(Y). When i click one of the blocks on a level, all other blocks shrink to some size, while the clicked one gets enlarged to the leftover space, and the sublevels show up on it's place.
For all the shrinking i'm using default animate function with easing effect, when shrinking 1 level, to avoid enlargement bugs i have to do something like this:
$tabs.not($obj).animate({height:32<<$obj.getVerUp().length+"px"},{duration:300,
step:function() {
$obj.height(function(){
var sum = 0;
$tabs.not($obj).each(function(){
sum += $(this).height();
});
return $obj.getCont().height()-sum+"px";
});
}
});
$tabs are all the tabs of current level, $obj - is the one tab that i want to enlarge
The main problem is:
When i open up a tab that is on a deep level, i have to animate all the tabs of higher levels to shrink a little bit more, thus the $obj X and Y would change, so the current animation has to use new values, but if i call 3 different animations on different levels i'm bound to get a bug, when one of the animations on a deeper level finishes 1 step earlier, while the one on the level above, would enlarge the object by 5-10 more pixels and that space wouldn't be used up.
The second problem is that there has to be about 50 object animating with easing at the same time, which is a little bit overkill.
And the last problem is when i call step callback on animation as shown above, i have a strange feeling that it calls the step separately for each animation of the $tabs collection, while i need 1 step for all the tabs in the list (to avoid unnecessary scripts)
There might be some other way to fix all that, but i have yet to discover all jQuery functions, so from what i see the only way is to simulate easing, and do everything in one single animation.
I don't really want to use setInterval and determining when do i need to clear it plus calculating each of the easing values, if there is a simple way doing it.
Does jQuery has some sort of empty animation easing, e.g.
$().css("height":starth+"px").animate({height:endh},{duration:300,
step:function(fn) {
// all the animation actions here via fn end value
}
});
Thanks in advance.
What I need - is not a completely working solution in code, just some enlightenment in those subjects:
Is there a legal way to call one step function for a collection of animated elements, or, maybe, it does call step once when I use one .animate on collection.
I'd be really appreciated if someone would shed some light over how does jquery handle multiple .animate, would they be used in one global function that works on .setInterval? or would they be having massive number of those .setIntervals that are equivalent to setTimeout (which most browsers can't handle in large amounts);
Is there a way to simulate 'animate' easing, some function name maybe, or a special trick to achieve that (the only thing I see is a hidden element or 'window' property to change maybe)
Or some directed pushes with functions I should study, that could help me achieve my goals
Guess i pretty much found the answer to my questions:
http://james.padolsey.com/javascript/fun-with-jquerys-animate/
Here's the empty animation from the link above with 1 step function with desired values, going to post the result later on if it all works out.
var from = {property: 0};
var to = {property: 100};
jQuery(from).animate(to, {
duration: 100,
step: function() {
console.log( 'Currently # ' + this.property );
}
});
Yes it all worked great, no desynch, and a good speed, since only 1 animate, found making one universal function for the animation - waste of resourses, so it is pretty specific, but still, here it is:
animate: function($obj) {
var T = this;
...
T.arr = new Array();
// gathering the array
$obj.each(function(i){
var size;
T.arr[i] = {obj:$(this), rest:$(this).getSibl(), cont:$(this).getCont()}
if($(this).hasClass("vert"))
{
size = "height";
T.arr[i].to = yto;
}
else
{
size = "width";
T.arr[i].to = xto;
T.arr[i].children = $(this).getChld();
}
T.arr[i].rest.each(function(){
$(this).attr("from",$(this)[size]());
});
});
// animating prop
jQuery({prop:0}).animate({prop:1}, {
duration: 300,
step: function() {
var i;
var P = this;
var newval;
var sum;
var size;
for(i = 0; i < T.arr.length; i++)
{
size = T.arr[i].obj.hasClass("vert") ? "height":"width";
sum = 0;
T.arr[i].rest.each(function(){
// new value of width/height, determined by the animation percentage
newval = parseInt($(this).attr("from")) + (T.arr[i].to-$(this).attr("from"))*P.prop;
$(this)[size](newval);
sum += newval;
});
T.arr[i].obj[size](T.arr[i].cont[size]()-sum);
}
}
});
},

Categories

Resources