Highcharts Is there a way to resize plotbands after zoom? - javascript

I'm doing something like this to resize my plotband when the chart renders:
var _Chart = new Highcharts.Chart( options, function ( chart ) {
for ( var i = 0; i < chart.xAxis[0].plotLinesAndBands.length; i++ ) {
var band = chart.xAxis[0].plotLinesAndBands[i].svgElem, path = band.d.split(' ');
var bottomString = chart.xAxis[0].plotLinesAndBands[i].options.iMBottom + '';
var topString = chart.xAxis[0].plotLinesAndBands[i].options.iMTop + '';
path[2] = bottomString;
path[path.length - 1] = bottomString;
path[5] = topString;
path[7] = topString;
band.attr('d', path);
}
});
That allows me to set the top and bottom of a chart's Y-Axis. We're displaying several Y-Axis in the same chart with some padding between them and each axis may have it's own plotbands. I don't want those to spill over into the other Axis since they look like smaller individual charts. I'm also changing the hover formatter to display a tooltip for these plotbands if a point within the band is hovered over.
The issue I'm having is that if you click, drag to zoom the plotbands revert back to the state where they bands go through the entire chart. I wrote some code to remove them and add them back in, but it seems like I'd need a callback on the addPlotBand function to change the SVG array before it's actually added and rendered. I've tried re-adding it then trying to redraw it, that didn't work.
Is there a way for me to adjust the SVG element when adding a new plotband so that I can alter the SVG element?
Edit:
Here's some more info. I tried using the selection event to delete all bands then call redraw, which calls a custom redraw event to put the SVG values back. Here's how that looks (the select event is first kinda crude but it works):
SelectChart: function (chart) {
var plotBandData = new Array();
for (var i = 0; i < chart.xAxis[0].plotLinesAndBands.length; i++) {
var band = chart.xAxis[0].plotLinesAndBands[i].svgElem, path = band.d.split(' ');
var plotBand = chart.xAxis[0].plotLinesAndBands[i];
//get all the plot band info so we can recreate them after resize
var pbd = {
from: plotBand.options.from,
to: plotBand.options.to,
id: plotBand.options.id,
bottom: plotBand.options.iMBottom,
top: plotBand.options.iMTop,
color: plotBand.options.color
};
plotBandData.push(pbd);
}
//delete all existing plot bands:
for (var i = 0; i < plotBandData.length; i++) {
chart.xAxis[0].removePlotBand(plotBandData[i].id);
}
for (var i = 0; i < plotBandData.length; i++) {
//Add plotband back in
chart.xAxis[0].addPlotBand({
from: plotBandData[i].from,
to: plotBandData[i].to,
color: plotBandData[i].color,
id: plotBandData[i].id,
iMBottom: plotBandData[i].bottom,
iMTop: plotBandData[i].top
});
}
},
Then my redraw event looks something like this:
RedrawChart: function (chart){
//SVG Code:
for (var i = 0; i < chart.xAxis[0].plotLinesAndBands.length; i++) {
var band = chart.xAxis[0].plotLinesAndBands[i].svgElem, path = band.d.split(' ');
var bottomString = chart.xAxis[0].plotLinesAndBands[i].options.iMBottom + '';
var topString = chart.xAxis[0].plotLinesAndBands[i].options.iMTop + '';
path[2] = bottomString;
path[path.length - 1] = bottomString;
/*JH: this is interesting, the second value for top is typically in the 8th spot in the array
However, before it's rendered the two right most "L"s are removed shifting everything to the left.
an example would be:
After render:
M 525 100 L 525 100 L 525 50 L 707 50 L 707 100
Before render (which is the state it's in here):
M 525 100 L 525 100 L 525 50 707 50 707 100
So you can see that the 8th value after render which is second top is shifted over one space.
The last value which is the second bottom is shifted over 2 spaces.
*/
path[5] = topString;
path[7] = topString;
band.attr('d', path);
}
},
My SVG values are correct at the end of redraw, however, the heights still get rendered to the full height of the chart.
Edit:
Now if I zoom in only the plotbands visible in the zoom stretch to the full height of the chart. Upon resetting zoom all all other plotbands are the correct size. Then if I select an area of the chart with no bands then click zoom out all the chart bands are correct. Seems to have something to do with only visible plotbands on select.

Here's what worked:
I created a new method that fires on the X-Axis afterSetExtremes() method (I suppose if you were doing this on the y-Axis you'd fire it there instead). I basically just took the code from the above two methods and stuck it all in there. Here's the code that fires off from the xAxis.afterSetExtremes() event:
AfterChartSetExteremes: function (xAxis) {
var plotBandData = new Array();
for (var i = 0; i < xAxis.plotLinesAndBands.length; i++) {
//get all the plot band info so we can recreate them after resize
var plotBand = xAxis.plotLinesAndBands[i];
var pbd = {
from: plotBand.options.from,
to: plotBand.options.to,
id: plotBand.options.id,
bottom: plotBand.options.iMBottom,
top: plotBand.options.iMTop,
color: plotBand.options.color
};
plotBandData.push(pbd);
}
//delete all existing plot bands:
for (var x = 0; x < plotBandData.length; x++) {
xAxis.removePlotBand(plotBandData[x].id);
}
for (var y = 0; y < plotBandData.length; y++) {
//Add plotband back in
xAxis.addPlotBand({
from: plotBandData[y].from,
to: plotBandData[y].to,
color: plotBandData[y].color,
id: plotBandData[y].id,
iMBottom: plotBandData[y].bottom,
iMTop: plotBandData[y].top
});
}
$.each(xAxis.plotLinesAndBands, function (i, pband) {
if (this.svgElem !== undefined) {
var band = this.svgElem;
path = band.d.split(' ');
var bottomString = this.options.iMBottom + '';
var topString = this.options.iMTop + '';
path[2] = bottomString;
path[path.length - 1] = bottomString;
/*JH: this is interesting, the second value for top is typically in the 8th spot in the array
However, before it's rendered the two right most "L"s are removed shifting everything to the left.
an example would be:
After render:
M 525 100 L 525 100 L 525 50 L 707 50 L 707 100
Before render (which is the state it's in here):
M 525 100 L 525 100 L 525 50 707 50 707 100
So you can see that the 8th value after render which is second top is shifted over one space.
The last value which is the second bottom is shifted over 2 spaces.
*/
path[5] = topString;
path[7] = topString;
this.svgElem.attr('d', path);
}
});
//Need to reset formatter here?
}
The xAxis from the function is "this" from the afterSetExtremes event, so I just passed it through like this:
function() {return PureSense.Clifford.IM.Charting.AfterChartSetExteremes(this);}
that works. All the plotbands get resized after zooming in or resetting zoom. I actually walked all the way through the highcharts code with the methods in my initial post. I saw that the SVG values did get set correctly and held all the way through. Something happens outside of redraw though and I couldn't catch it.
I do know it happens though because if you look at my comments the "d" array is two items short of the SVG element on the page (if you view it with F12 dev tools). The afterSetExtreme event fires after redraw and after the zoom happens. I think writing those values during redraw causes them to get overwritten when the zoom finalizes.
Hope that's helpful to someone else.

Related

Don't understand what this code does

So I know this code is randomly removing to of my objects to create a hole so another object can go through but line by line I would like to go through and understand each part. Would be great if someone not so arrogant could help me because I'm new. I would appreciate any help. The area I don't understand is the last part which I have highlighted in bold. Thank you.
// Add a pipe on the screen
add_one_pipe: function(x, y) {
// Get the first dead pipe of our group
var pipe = this.pipes.getFirstDead();
// Set the new position of the pokeballs
pipe.reset(x, y);
// Add velocity to the pokeballs to make it move left
pipe.body.velocity.x = -200;
// Kill the pokeballs when it's no longer visible
pipe.outOfBoundsKill = true;
},
**add_row_of_pipes: function() {
var hole = Math.floor(Math.random()*5)+1;**
**for (var i = 0; i < 8; i++)
if (i != hole && i != hole +1)
this.add_one_pipe(400, i*60+10);**
add_row_of_pipes will add 6 pipes at fixed intervals heights, but with a randomly placed gap of 2 missing pipes.
var hole = Math.floor(Math.random()*5)+1;
Take a random number (between 0 and 0.999),
Multiply by 5 (possible range is now 0-4.9999...),
Round down (0-4),
Add one (1-5)
This value is where the hole is
for (var i = 0; i < 8; i++)
For the whole numbers 0 to 7 inclusive, representing height, i...
if (i != hole && i != hole +1)
If this height is not where the hole starts, nor the next value,
this.add_one_pipe(400, i*60+10);
Add a pipe at width 400, and height i*60+10.

Programmatically creating labels for Chart.js in angular?

I am dynamically creating a line graph for a movie. The y axis is a number of values that is collected over the course of the movies length. The length of the movie is known before. My problem is that ChartJs needs meaningful labels. It also puts each label at the same point as a y axis coordinate so I have to have the same amount of labels as points on the y axis.
This is the code I tried to use to programmatically created an array of labels based on the length of the data that I am feeding ChartJS
var makeLabels = function(){
var numberOfSegments = $scope.data.length;
$scope.labelArray = [];
for (var i = 0; i = numberOfSegments; i++) {
var label = i * 10;
var labelString = label.toString();
$scope.labelArray.push(labelString);
};
}
Image
this is a graph without all of the y axis data.
I fixed it do display the way I want to. The function makeLabels() gets the length of the movie and adds a blank "" label for every label that is not a multiple of 10. I removed the grid lines with scaleShowGridLines: false in the ChartJsProvider.setOptions method.
var makeLabels = function(){
var segments = $scope.movieLength / 1;
s
for (var i = 0; i <= segments; i++) {
var modulus = i % 10;
if(modulus === 0){
$scope.labelsArray.push(i.toString());
} else {
$scope.labelsArray.push("");
};
};

How to set the dynamic or static tick size in a Rickshaw.js plot?

I am creating a Rickshaw.js-powered graph much like in this example: http://code.shutterstock.com/rickshaw/tutorial/example_07.html based on my own data that is returned via an AJAX call. The data is either measured in bytes (typical values range in a few gigabytes or hundreds of MBs) or seconds (anywhere between 10s and 50 minutes). I tried using a Rickshaw.Fixtures.Number.formatBase1024KMGTP formatter for the bytes and wrote my own for the seconds, which does its part well. The problem is that I need to position the tick lines in a smart way - preferably dynamically, but even static settings (e.g. place a tick every 1024*1024*1024=1 GB or every 60 s) would be fine.
I tried setting the tickSize to 1024^3 like so:
var y_axis = new Rickshaw.Graph.Axis.Y({
graph: graph,
tickSize: 1073741824 // 1 GB
});
y_axis.render();
but I ended up seeing no ticks at all. What am I doing wrong and what would be the right way?
Basically, you need to adapt the tickOffsets() function of the Axis.X, Axis.Y and Axis.Time classes in Rickshaw.
tickSize will not help you with that as - like #Old Pro stated correctly - it indicates the size of the bold tick lines in pixels. It has nothing to do with spacing.
For Time-based Axes
My solution essentially consists of replacing the standard tickOffsets() function in those files
this.tickOffsets = function() {
var domain = this.graph.x.domain();
var unit = this.fixedTimeUnit || this.appropriateTimeUnit();
var count = Math.ceil((domain[1] - domain[0]) / unit.seconds);
var runningTick = domain[0];
var offsets = [];
for (var i = 0; i < count; i++) {
var tickValue = time.ceil(runningTick, unit);
runningTick = tickValue + unit.seconds / 2;
offsets.push( { value: tickValue, unit: unit } );
}
return offsets;
};
by a custom routine. This is gonna do the trick:
this.tickOffsets = function() {
var domain = this.graph.x.domain();
var unit = this.fixedTimeUnit || this.appropriateTimeUnit();
var tickSpacing = args.tickSpacing || unit.seconds;
var count = Math.ceil((domain[1] - domain[0]) / tickSpacing);
var runningTick = domain[0];
var offsets = [];
for (var i = 0; i < count; i++) {
var tickValue = time.ceil(runningTick, unit);
runningTick = tickValue + tickSpacing;
offsets.push( { value: tickValue, unit: unit } );
}
return offsets;
};
With that in place, you can write something like
var time = new Rickshaw.Fixtures.Time();
var timeUnit = time.unit('year');
var x_axis = new Rickshaw.Graph.Axis.ExtendedTime(
{
graph: graph,
tickSpacing: 60*60*24*365*13, // 13 years
timeUnit: timeUnit
} );
to have ticks spaced out evenly at every 13 years.
For Value-based Axes
For value-based Axes, you would need to extend the render() function to include a facility that "manually" sets the ticks for the axis. I did it like this:
this.render = function() {
if (this.graph.height !== this._renderHeight) this.setSize({ auto: true });
var axis = d3.svg.axis().scale(this.graph.y).orient(this.orientation);
if (this.tickSpacing) {
var tickValues = [];
var min = Math.ceil(axis.scale().domain()[0]/this.tickSpacing);
var max = Math.floor(axis.scale().domain()[1]/this.tickSpacing);
for (i = min * this.tickSpacing; i < max; i += 1) {
console.log(i);
tickValues.push(i * this.tickSpacing);
}
axis.tickValues(tickValues);
}
axis.tickFormat( args.tickFormat || function(y) { return y } );
if (this.orientation == 'left') {
var berth = this.height * berthRate;
var transform = 'translate(' + this.width + ', ' + berth + ')';
}
if (this.element) {
this.vis.selectAll('*').remove();
}
this.vis
.append("svg:g")
.attr("class", ["y_ticks", this.ticksTreatment].join(" "))
.attr("transform", transform)
.call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));
var gridSize = (this.orientation == 'right' ? 1 : -1) * this.graph.width;
this.graph.vis
.append("svg:g")
.attr("class", "y_grid")
.call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize));
this._renderHeight = this.graph.height;
};
The important part here are the statements in the if (this.tickSpacing) clause. They compute ticks given by the tickSpacing variable in the config array, and assign them to the axis in the axis.tickValues(tickValues) statement. Note that this.tickValues is assigned in the this.tickSpacing = args.tickSpacing statement in the initialize() function, not stated above.
Try it yourself
Have a look at this jsfiddle, where the complete code is available. This will certainly give you some pointers. If you want, you can create your own jsfiddle with your values and tell me if you need anything else.
tickSize is the size of the ticks in pixels. Not what you want to be setting to a huge number.
Set ticks to the number of ticks you want on the graph and Rickshaw (actually d3) will do some magic to give you pretty values of ticks that generate about that number of ticks on the graph.
If you want further control you're going to have to dig into d3, where you will be able to explicitly set the tick values using axis.tickValues(). I'd probably copy the existing Rickshaw.Graph.Axis.Y code and create my own Y axis class that includes access to tickValues or the ability to use my own scale. It's a little unclean in that Rickshaw creates the Y scale in the graph.render() function, so you can't easily override the Y scale, but the Y scale Rickshaw creates does have the range set from the graph data, which is information you will want when creating your own tick values.

How to find selected elements within a javascript marquee selection box without using a loop?

I am writing my own drag and drop file manager. This includes a javascript marquee selection box which when active calculates the elements (files) that are intersected and selects them by adding a class to them.
I currently perform the check during a mousemove handler, loop through an array of element coordinates and determine which ones are intersected by the drag and drop selection box.
The function currently looks like this:
selectItems : function(voidindex){
var self = this;
var coords = self.cache.selectioncoords;
for(var i=0, len = self.cache.items.length; i<len; i++){
var item = self.cache.items[i];
var itemcoords = item.box_pos;
if(coords.topleft.x < (itemcoords.x+201) && coords.topright.x > itemcoords.x && coords.topleft.y < (itemcoords.y+221) && coords.bottomleft.y > itemcoords.y){
if(!item.selected){
item.selected = true;
item.html.addClass('selected').removeClass('activebutton');
self.cache.selecteditems.push(i);
self.setInfo();
}
}
else{
if(item.selected){
item.selected = false;
if(!voidindex || voidindex !== i){
item.html.removeClass('selected');
}
var removeindex = self.cache.selecteditems.indexOf(i);
self.cache.selecteditems.splice(removeindex, 1);
self.setInfo();
}
}
}
},
There is lots of dirty logic in the code above which ensures that the DOM is only manipulated when the selection changes. This is not relevant to the question and can be exluded. The important part is the intersection logic which checks the coordinates of the element versus the coordinates of the marquee selection box.
Also please note that the item dimensions are fixed at 201px width by 221px height.
I have tested this and all works perfectly, however I have the need to support potentially thousands of files which would mean that at some point we will start seeing UI performance decrease.
I would like to know if there is anyway to perform intersection detection without looping through the coordinates of each element.
The coordinates of the marquee box are defined as follows at any given time:
selectioncoords : {
topleft : {
x : 0,
y : 0
},
topright : {
x : 0,
y : 0
},
bottomleft : {
x : 0,
y : 0
},
bottomright : {
x : 0,
y : 0
},
width : 0,
height : 0
}
And the coordinates of each item, stored in the self.cache.items array are defined as follows:
item : {
box_pos : {
x : 0,
y : 0
},
grid_pos : {
row : 1,
column : 1
}
}
So the information available will always be the actual grid position (row/column) as well as the physical item position (left and top offsets in pixels within the grid).
So to summarize, the question is, is there anyway to detect item intersection from a set of marquee selection box coordinates as defined above without looping through the whole array of item coordinates every time the mousemove event fires?
Thanks in advance for any help.
The following depends upon a locked grid with the dimensions as described.
You are comparing a mouse-defined rectangle against a grid with static edge sizes. Thus, given an x coordinate or a y coordinate, you should be able to derive pretty easily which column or row (respectively) the coordinate falls into.
When the user starts the select box, grab that x and y, and find the row/column of the start. When the mouse moves while pulling the select box, you find (and then update) the row/column of the finish. anything that is both within the rows defined by that box and within the columns defined by that box (inclusive) is selected. If you then keep your selectable elements in a two-dimensional array according to rows and columns, you should be able to just grab the ones you want that way.
Mind, how much more (or less) efficient this is depends on the size of your expected selection boxes as compared to the total size, and the degree to which you expect the grid to be populated. Certainly, if the average use case is selecting half or so of the objects at a time, there's not a whole lot you can do to cut down efficiently on the number of objects you have to look at each time.
Also, though it is kludgy, you can have the mousemove handler not fire every time. Letting it pause a bit between updates will reduce the responsiveness of this particular function a fair bit, but it'll cut down significantly on the amount of resources that are used.
There are several ways you could approach this. Here's one. First you need the items in some kind of organized structure that you can look up quickly by row and column. You could use a two-dimensional array, or for simplicity I'm going to use a hash table. You could do this at the same time that you create the self.cache.items, or later, something like this:
var cacheLookup = {};
function initCacheLookup() {
var items = self.cache.items;
for( var i = 0, n = items.length; i < n; i++ ) {
var item = items[i];
var key = [ item.grid_pos.row, item.grid_pos.column ].join(',');
cacheLookup[key] = item;
}
}
Then when you want to get the items intersecting the rectangle, you could do something like this:
var itemWidth = 201, itemHeight = 221;
var tl = selectioncoords.topleft, br = selectioncoords.bottomright;
var left = Math.floor( tl.x / itemWidth ) + 1;
var right = Math.floor( br.x / itemWidth ) + 1;
var top = Math.floor( tl.y / itemHeight ) + 1;
var bottom = Math.floor( br.y / itemHeight ) + 1;
var selecteditems = [];
for( var row = top; row <= bottom; row++ ) {
for( var col = left; col <= right; col++ ) {
var key = [ row, col ].join(',');
var item = cacheLookup[key];
if( item ) {
selecteditems.push( item );
}
}
}
// Now selecteditems has the items intersecting the rectangle
There's probably an off-by-one error or two here, but this should be close.
Well, as I said, that is one way to do it. And it has the possibly interesting property that it doesn't depend on the order of items in the self.cache.items array. But that cacheLookup hash table smells like it might not be the most efficient solution.
Let me take a guess: isn't that array already in the correct order by rows and columns (or vice versa)? For example, if your grid is four wide, then the top row would be array elements 0-3, the second row 4-7, the third row 8-11, etc. Or it could be a similar arrangement going down the columns.
Assuming it's in row-by-row order, then you don't need the hash table at all. That initCacheLookup() function goes away, and instead the search code looks like this:
var nCols = 4/*whatever*/; // defined somewhere else
var itemWidth = 201, itemHeight = 221;
var tl = selectioncoords.topleft, br = selectioncoords.bottomright;
var left = Math.floor( tl.x / itemWidth );
var right = Math.floor( br.x / itemWidth );
var top = Math.floor( tl.y / itemHeight ) * nCols;
var bottom = Math.floor( br.y / itemHeight ) * nCols;
var items = self.cache.items;
var selecteditems = [];
for( var iRow = top; iRow <= bottom; iRow += nCols ) {
for( var col = left; col <= right; col++ ) {
var index = iRow + col;
if( index < items.length ) {
selecteditems.push( items[index] );
}
}
}
// Now selecteditems has the items intersecting the rectangle
This code will be a little faster, and it's simpler too. Also it doesn't depend at all on the item.box_pos and item.grid_pos. You may not need those data fields at all, because they are easily calculated from the item index, grid column count, and item height and width.
Some related notes:
Don't hard code 201 and 221 in the code. Store those in variables once, only, and then use those variables when you need the item height and width.
There is a lot of duplication in your data structures. I recommend that you ruthlessly eliminate all duplicated data unless there is a specific need for it. Specifically:
selectioncoords: {
topleft: {
x: 0,
y: 0
},
topright: {
x: 0,
y: 0
},
bottomleft: {
x: 0,
y: 0
},
bottomright: {
x: 0,
y: 0
},
width: 0,
height: 0
}
More than half the data here is duplicated or can be calculated. This is all you need:
selectioncoords: {
left: 0,
right: 0,
top: 0,
bottom: 0
}
The reason I bring this up is that was a bit confusing when working on the code: "I want the left edge. Do I get that from topleft.x or bottomleft.x? Are they really the same like they seem? How do I pick?"
Also, as mentioned above, the item.box_pos and item.grid_pos may not be needed at all if the items are stored in a sequential array. If they are needed, you could store just one and calculate the other from it, since there's a direct relationship between the two:
box_pos.x === ( grid_pos.column - 1 ) * itemWidth
box_pos.y === ( grid_pos.row - 1 ) * itemHeight
You can limit the scope of your checks by indexing each item in a grid, as often as necessary and no more often. You can use the grid to give you a list of elements near an X, Y coordinate or that might be in an X1, Y2, X1, Y2 range.
To get you started ...
var Grid = function(pixelWidth, pixelHeight, boxSize) {
this.cellsIn = function(x1, y1, x2, y2) {
var rv = [];
for (var x = x1; x < x2; x += boxSize) {
for (var y = y1; y < y2; y += boxSize) {
var gx = Math.ceil(x/boxSize);
var gy = Math.ceil(y/boxSize);
rv.push(this.cells[gx][gy]);
}
}
return rv;
} // cellsIn()
this.add = function(x1, y1, x2, y2, o) {
var cells = this.cellsIn(x1, y1, x2, y2);
for (var i in cells) {
cells[i].push(o);
}
} // add()
this.get = function(x1, y1, x2, y2) {
var rv = [];
var rv_index = {};
var cells = this.cellsIn(x1, y1, x2, y2);
for (var i in cells) {
var cell = cells[i];
for (var oi in cell) {
if (!rv_index[cell[oi]]) {
rv_index[cell[oi]] = 1;
rv.push(cell[oi]);
}
}
}
return rv;
} // get()
this.cells = [];
for (var x = 0; x < Math.ceil(pixelWidth/boxSize); x++) {
this.cells[x] = [];
for (var y = 0; y < Math.ceil(pixelHeight/boxSize); y++) {
this.cells[x][y] = [];
}
}
};
So, rather than iterating through all possible objects, whatever they may be, you iterate over all the objects that are near or potentially in the given coordinates.
This requires that you maintain/re-index the grid as item coordinates change. And you'll likely want to add some functionality to the above (or similar) Grid class to modify/move existing objects. But, to the best of my knowledge, an index of this sort is the best, if not only, way to index objects "in space."
Disclaimer: The code above isn't tested. But, I have similar code that is. See the DemoGrid function class here: http://www.thepointless.com/js/ascii_monsters.js
The functionality of my DemoGrid is similar (as far as I remember, it's been awhile), but accepts x, y, radius as parameters instead. Also notable, my mouse events don't touch the grid every time the event fires. Checks are rate-limited by a game/main loop.
If the system is set up such that
self.cache.items is ordered from left to right and top to bottom
(0,0),(1,0),(2,0),(0,1),(1,1),(1,2),(0,2),(1,2),(2,2)
There is an item in each space
GOOD - (0,0),(1,0),(2,0),(0,1),(1,1),(1,2),(0,2),(1,2),(2,2)
BAD - (0,0),(2,0)(1,2),(1,3),(2,1),(2,3)
We need to know the total number of columns.
So the code to get you started.
// Some 'constants' we'll need.
number_of_columns = 4;
item_width = 201;
item_height = 221;
// First off, we are dealing with a grid system,
// so that means that if given the starting x and y of the marquee,
// we can determine which element in the cache to start where we begin.
top_left_selected_index = Math.floor(selectioncoords.topleft.x / item_width) + (Math.floor(selectioncoords.topright.y / item_height) * number_of_columns );
// Now, because the array is in order, and there are no empty cache points,
// we know that the lower bound of the selected items is `top_left_selected_index`
// so all we have to do is walk the array to grab the other selected.
number_columns_selected = (selectioncoords.bottomright.x - selectioncoords.topleft.x) / item_width;
// if it it doesn't divide exactly it means there is an extra column selected
if((selectioncoords.bottomright.x - selectioncoords.topleft.x) % item_width > 0){
number_columns_selected += 1;
}
// if it it doesn't divide exactly it means there is an extra column selected
number_rows_selected = (selectioncoords.bottomright.y - selectioncoords.topleft.y) / item_height;
if((selectioncoords.bottomright.y - selectioncoords.topleft.y) % item_height > 0){
number_rows_selected += 1;
}
// Outer loop handles the moving the pointer in terms of the row, so it
// increments by the number of columns.
// EX: Given my simple example array, To get from (1,0) to (1,1)
// requires an index increase of 3
for(i=0; i < number_rows_selected; i++){
// Inner loop marches through the the columns, so it is just one at a time.
// Added j < number_of_columns in case your marquee stretches well past your content
for(j=0; j < number_columns_selected && j < number_of_columns; j++){
// Do stuff to the selected items.
self.cache.items[top_left_selected_index + (i * number_of_columns) + j];
}
}

Assign index # according to current section

Say I have a total width of 585px. And I wanted to divide the space into equal sections and assign each an index value within position. I could do something like this if I had lets say 6 sections: (assigned by total width / number of sections)
//Set up elements with variables
this.sliderContent = config.sliderContent;
this.sectionsWrap = config.sectionsWrap;
//Selects <a>
this.sectionsLinks = this.sectionsWrap.children().children();
//Create drag handle
this.sectionsWrap.parent().append($(document.createElement("div")).addClass("handle-containment")
.append($(document.createElement("a")).addClass("handle ui-corner-all").text("DRAG")));
//Select handle
this.sliderHandle = $(".handle");
var left = ui.position.left,
position = [];
var position = ((left >= 0 && left <= 80) ? [0, 1] :
((left >= 81 && left <= 198) ? [117, 2] :
((left >= 199 && left <= 315) ? [234, 3] :
((left >= 316 && left <= 430) ? [351, 4] :
((left >= 431 && left <= 548) ? [468, 5] :
((left >= 549) ? [585, 6] : [] ) ) ) ) ) );
if (position.length) {
$(".handle").animate({
left : position[0]
}, 400);
Slider.contentTransitions(position);
}
But what if I had an x number of sections. These sections are just elements like
<li><a></a></li>
<li><a></a></li>
<li><a></a></li>
Or
<div><a></a></div>
<div><a></a></div>
<div><a></a></div>
<div><a></a></div>
How would I divide the total of 585px and classify the index in position according to the current left value of the .handle element? I can know where the drag handle is by using ui.position.left, what I want is to be able to set an index for each element and be able to animate handle depending on where the handle is within the indexed elements. Since each element is indexed I later call a transition method and pass in the current index # to be displayed. The code I show above works, but isn't really efficient. I also need to account for the width of the handle to fit the section width. http://jsfiddle.net/yfqhV/1/
Ok, there is a slight inconsistency in the difference between the range figures in the question, which makes it hard to algorithmise [ my made-up-word de jour =) ] this exactly:
81 to 199 = 118
199 to 316 = 117
316 to 431 = 115
431 to 518 = 118
If you can adjust for that, I have a solution - it's not especially clever JavaScript, so there may well be better ways to do this (SO JS people, feel free to educate me!) but it works.
First we need a function to find the index of an array range, a given value falls within (this replaces your nested if-else shorthands), then we have a function to set up the positional arrays, and finally we can do a range search and return the corresponding array of values.
This solution should dynamically deal with a varying number of sections, as long as this line:
var len = $("#sectionContainer").children().length;
is adjusted accordingly. The only other values that may need adjusting are:
var totalWidth = 585;
var xPos = 81;
although you could set them if you have elements you can draw the values from, making it even more of a dynamic solution.
/**
* function to find the index of an array element where a given value falls
* between the range of values defined by array[index] and array[index+1]
*/
function findInRangeArray(arr, val){
for (var n = 0; n < arr.length-1; n++){
if ((val >= arr[n]) && (val < (arr[n+1]))) {
break;
}
}
return n;
}
/**
* function to set up arrays containing positional values
*/
function initPositionArrays() {
posArray = [];
leftPosArray = [];
var totalWidth = 585;
var xPos = 81;
var len = $("#sectionContainer").children().length;
var unit = totalWidth/(len - 1);
for (var i=1; i<=len; i++) {
pos = unit*(i-1);
posArray.push([Math.round(pos), i]);
xMin = (i >= 2 ? (i==2 ? xPos : leftPosArray[i-2] + posArray[1][0]) : 0);
leftPosArray.push(Math.round(xMin));
}
}
var left = ui.position.left;
initPositionArrays();
// find which index of "leftPosArray" range that "left" falls within
foundPos = findInRangeArray(leftPosArray, left);
var position = posArray[foundPos];
if (position.length) {
$(".handle").animate({
left : position[0]
}, 400);
Slider.contentTransitions(position);
}
I've set up a jsFiddle to illustrate.
Enjoy!
Edit
I've looked at #JonnySooter s own answer, and whilst it calculates the positioning correctly, it won't deal with a variable number of sections.
To get it to work with any number of sections, the handleContainment div (that is created on-the-fly) needs to have it's width set dynamically (via inline styling).
This is calculated by multiplying the number of sections by the width of each section (which is actually the same as the width of the slider).
This is all done after creating the handle so that the width can be extracted from the "handle" css class, meaning a change to the width of the handle will cascade into the routine when applied at the css level.
See this jsFiddle where the number of sections can be altered and the slider behaves properly.
var numSections = // ...;
var totalWidth = // ...;
var sectionWidth = totalWidth / numSections;
var index = Math.floor($(".handle").position().left / sectionWidth);
var leftPosition = index * sectionWidth;
var rightPosition = leftPosition + sectionWidth - 1;
UPDATE:
I worked on trying to find a solution myself and this is what I came up with:
function( event, ui ) {
var left = ui.position.left, //Get the current position of the handle
self = Slider, //Set to the Slider object cus func is a callback
position = 1;
sections_count = self.sectionsLinks.length, //Count the sections
section_position = Math.floor(self.sectionsWrap.width() / sections_count); //Set width of each section according to total width and section count
left = Math.round(left / section_position); //Set the index
position = (left * section_position); //Set the left ammount
if(position < section_position){ //If handle is dropped in the first section
position = 0.1; //Set the distance to animate
left = 0; //Set index to first section
}
if (position.length) {
$(this).animate({
left : position //Animate according to distance
}, 200);
left = left += 1; //Add one to the index so that I can use the nth() child selector later.
self.contentTransitions(left);
}
}

Categories

Resources