Equalizing Element Heights with JavaScript - javascript

I am writing Javascript that will take an element with the class of "eqlizm", get the .offsetHeight of all the .children, determine the max value, and then set the heights of all the .children to that value.
My problem is that while I can get it to echo the values out, it is only setting them for the first child.
I have the code setup on CodePen.io but here is the script, complete with my diagnostic console.logs:
var eqlizmContainers = document.querySelectorAll(".eqlizm");
function getArrMax(numArray) {
return Math.max.apply(null, numArray);
}
function findMaxHeight(targetContainer) {
var eqlizNodes = targetContainer.children;
var childHeights = [];
for (c = 0; c < eqlizNodes.length; c++) {
childHeights.push(eqlizNodes[c].offsetHeight);
}
return getArrMax(childHeights);
}
function eqlizHeight(targetContainer) {
var eqlizNodes = targetContainer.children;
//console.log(eqlizNodes);
for (c = 0; c < eqlizNodes.length; c++) {
//console.log(targetContainer + " " + c);
//console.log(eqlizNodes[c]);
eqlizNodes[c].style.height = findMaxHeight(targetContainer) + "px";
}
}
for (i = 0; i < eqlizmContainers.length; i++) {
//console.log("Tier " + (i+1) + ": " + findMaxHeight(eqlizmContainers[i]) + "px");
eqlizHeight(eqlizmContainers[i]);
}
After I get this function working, I will be adding event listeners for the resizing of the browser window.
I have looked high and low for a solution but all of the ones I could find involve jQuery which I want to avoid using that.

The biggest problem with the code you posted is that you're misunderstanding variable scoping in JavaScript.
All variables are function-scoped, regardless of the block in which they're initialized.
Your c = 0 is being saved in the global scope.
window.c; //0
Thus, inside of eqlizHeight, you're setting c to 0, and then for each call to findMaxHeight (which looks like a pretty big de-op, that would be better suited to run outside of the loop, and be stored once per parent), you're running the value of c up to the length of the children of the first parent.
The quick and dirty solution is to add var c = 0; to your variable declarations in the functions.
Then your for constructs can look like for (; c < ...; c++).
I prefer working in a functional fashion, without explicit looping, so I had a quick run at a second solution:
// turns an array-like into an array (el.children, document.querySelectorAll(...), etc)
function slice (arrlike, start, end) {
return [].slice.call(arrlike, start, end);
}
// takes a property name and returns a reusable function
// var getName = pluck("name");
// getName({ name: "Bob" }); // Bob
// [{ name: "Bob" }, { name: "Doug" }].map(getName); // ["Bob", "Doug"]
function pluck (key) {
// returns a function, which will pull the same property from any object it gets
return function (obj) { return obj[key]; };
}
// it's just faster calling query(selector); than document.querySelector(selector);
function query (selector) {
return document.querySelector(selector);
}
// same as above, but I also turn the resulting HTMLCollection into an Array
function queryAll (selector) {
return slice(document.querySelectorAll(selector));
}
// take an array of numbers and return the max
function max (numbers) {
numbers = Array.isArray(numbers) ? numbers : slice(arguments);
return Math.max.apply(Math, numbers);
}
// take an array of numbers and return the min
function min (numbers) {
numbers = Array.isArray(numbers) ? numbers : slice(arguments);
return Math.min.apply(Math, numbers);
}
// returns an object with top, right, bottom, left, width, height in pixels
// (measured from the visible part of the page, so scrolling affects the numbers)
function getBoundingClientRect (el) {
return el.getBoundingClientRect();
}
// takes a style-name, and value, and returns a reusable function
// var setHeightTo100 = setStyle("height", "100px");
// setHeightTo100(el_1);
// setHeightTo100(el_2);
function setStyle (key, value) {
// sets the same el.style[key] = value; for every element it's passed
return function (el) { el.style[key] = value; };
}
// takes an array of elements
function resizeChildren (nodes) {
nodes = Array.isArray(nodes) ? nodes : slice(arguments);
// for each element ("container")
nodes.forEach(function (container) {
// get the container's children as an array
var children = slice(container.children);
// get the max( ) of the .height properties of the boundingClientRects of the children
var maxHeight = max(children.map(getBoundingClientRect).map(pluck("height")));
// set style.height = maxHeight + "px" on each child
children.forEach(setStyle("height", maxHeight + "px"));
});
}
// for a list of all ".eqlizm" children, resizeChildren
resizeChildren(queryAll(".eqlizm"));
If you paste that into your pen, I think you'll find that it works just fine.
Edit: illustrating the quick & dirty fix to the preexisting code
var eqlizmContainers = document.querySelectorAll(".eqlizm");
var i = 0;
function getArrMax(numArray) {
return Math.max.apply(null, numArray);
}
function findMaxHeight(targetContainer) {
var eqlizNodes = targetContainer.children;
var childHeights = [];
var c = 0;
for (c = 0; c < eqlizNodes.length; c++) {
childHeights.push(eqlizNodes[c].offsetHeight);
}
return getArrMax(childHeights);
}
function eqlizHeight(targetContainer) {
var eqlizNodes = targetContainer.children;
var c = 0;
for (c = 0; c < eqlizNodes.length; c++) {
eqlizNodes[c].style.height = findMaxHeight(targetContainer) + "px";
}
}
for (i = 0; i < eqlizmContainers.length; i++) {
eqlizHeight(eqlizmContainers[i]);
}
Other than removing the comments, all I've done is added two var c; lines.
Whether you initialize var c to a value or not doesn't matter; all that matters is that you use var to announce it.
Otherwise, it gets saved in the global scope.
Sure, you could just keep using a, b, d, e, f, g, h, j, ..., but that means that any time two pieces of code on the same page use x, one will overwrite the other...
var c; inside of the function tells JavaScript that c's home is there, and not to look any higher up.

Assign them all targets a similar class and loop through them.
$(document).ready(function(){
var height = 0;
$(".homepage-product").each(function(){
if ($(this).height() > height){
height = $(this).height()
}
});
$(".homepage-product").each(function(){
$(this).height(height);
});
});
JSFIDDLE example

Related

In angular updating one variable inexplicably updates another

I am using angular and plotly to plot either the raw data or a moving average. I have the moving average working but I am running into an issue with assigning variables. I retrieve an array of user objects which each have an x and y key with arrays associated with them.
$scope.init=function(){
$rootScope.page='companyResults';
$scope.isPlotlyDone = false;
$scope.moving = false;
var refresh = function () {
incidentService.dayWiseTripsByUser(...).then(function (plotArray){
$scope.unaffectedPlot = plotArray;
$scope.movingAveragePlot = allMoving(plotArray);
console.log($scope.unaffectedPlot[0].y);
console.log($scope.movingAveragePlot[0].y);
});
};
refresh();
}
Im that code block, I would expect that $scope.unaffectedPlot[0].y and $scope.movingAveragePlot[0].y would have different arrays since I ran the latter through the following set of functions. The curious thing is that both $scope variables are synced, so if I run the second through allMoving the unaffectedPlot variable also gets smoothed and neither get synced obviously if I don't call allMoving. What am I missing about Angular? What is a good way to have a moving average work with a toggle? My plan is to show one variable or the other depending on if a button is clicked.
var d3_numeric = function(x) {
return !isNaN(x);
}
var d3sum = function(array, f) {
var s = 0,
n = array.length,
a,
i = -1;
if (arguments.length === 1) {
// zero and null are equivalent
while (++i < n) if (d3_numeric(a = +array[i])) s += a;
} else {
while (++i < n) if (d3_numeric(a = +f.call(array, array[i], i))) s += a;
}
return s;
};
var movingWindowAvg = function (arr, step) {
return arr.map(function (_, idx) {
var wnd = arr.slice(idx - step, idx + step + 1);
var result = d3sum(wnd) / wnd.length; if (isNaN(result)) { result = _; }
return result;
});
};
var allMoving = function(pltArray) {
var movingArray = [];
pltArray.forEach(function(plot){
var oneMoving = plot;
oneMoving.y = movingWindowAvg(plot.y, 5);
movingArray.push(oneMoving);
});
return movingArray;
}
This actually isn't an angular issue. I had to test it some since I didn't see what was going on either.
When you wrote
oneMoving.y = blah
you were actually altering the contents of plot for each element and in turn altering the contents of plotArray unintentionally (since plot is an object)
So you are only creating a reference variable when you say 'var onMoving = plot' )
To outright solve your problem you can clone plot but that isn't so clean of a process
One easy yet dirty way is
JSON.parse(JSON.stringify(obj))
from this thread
I threw together a shotty example that captures what was going wrong for you
var array = [{one:1, two:2},{one:1, two:2},{one:1, two:2}],
copyArray = array,
newArr = doStuff(array)
function doStuff(a) {
var otherNewArr = []
a.forEach(function(ae) {
var aVar = ae
aVar.one = 5
otherNewArr.push(aVar)
})
return otherNewArr
}
console.log(copyArray,newArr)
And to fix it just replace
var aVar = ae
with
var aVar = JSON.parse(JSON.stringify(ae))

Javascript looping through large dictionary yields unresponsive script

I'm making a firefox addon that appends words with other words in html text. This code works, but when iterating through an enormous dictionary, I get an unresponsive script error.
What is the best way to improve the speed of this loop?
Splitting up the dictionary into smaller objects? Or setting a timeout function?
var brands = {"manykeys" : "manyvalues"};
function replaceWord(){
for (var key in brands){
htmlreplace(key, key + " (" + brands[key] + ")");
}
}
function htmlreplace(a, b, element) {
if (!element) element = document.body;
var nodes = element.childNodes;
for (var n=0; n<nodes.length; n++) {
if (nodes[n].nodeType == Node.TEXT_NODE) {
var r = new RegExp(a, 'g');
nodes[n].textContent = nodes[n].textContent.replace(r, b);
} else {
htmlreplace(a, b, nodes[n]);
}
}
}
replaceWord();
There are some considerations to take. It depends a lot on what you can change or not. One of the bigger improvements you can do is using array instead of key/value object.
var brands = [
['manykeys0000','manyvalues0000'],
['manykeys0001','manyvalues0001'],
['manykeys0002','manyvalues0002'],
['manykeys0003','manyvalues0003'],
['manykeys0004', ...
];
function replaceWord(){
var i, n = brands.length;
for (i = 0; i < n; ++i) {
htmlreplace(brands[i][0], brands[i][0] + " (" + brands[i][1] + ")");
}
}
A couple of other changes should also give a tiny improvement:
1.) Move nodes.length outside the loop.
2.) If suitable pass document.body from replaceWord()
var body = document.body;
...
htmlreplace(brands[i][0], brands[i][0] + " (" + brands[i][2] + ")", body);
function htmlreplace(a, b, element) {
var nodes = element.childNodes, len = nodes.length;
for (var n=0; n < len; ++n) {
Combined quick benchmark on Chrome and Firefox gave a 30-40% increase in speed.
Other edits to test out:
Move var r = new RegExp(a, 'g'); to replaceWord(), and pass it as first argument to htmlreplace() instead of a.
function replaceWord(){
var i, n = brands.length;
for (i = 0; i < n; ++i) {
var r = new RegExp(brands[i][0], 'g');
htmlreplace(r, brands[i].join(' (') + ')', elem);
}
}
If you play with timeout this article might be of interest. It uses
window.postMessage();
to implement a custom setZeroTimeout() but not sure how it will impact your case.
Beside JSPerf etc. use profiling tools in browser, for example in Chrome, which might be better suited for what you do in your code.
I haven't tried, but doing this might work:
function replaceWord(){
for (var key in brands){
(function(key) {
setTimeout(function() {
htmlreplace(key, key + " (" + brands[key] + ")");
}, 0);
})(key);
}
}
The idea is that you postpone the replacing for when the browser has time for it, instead of doing it monolithically and making the browser freeze while it's thinking.
The bottleneck in your code is not the dictionary size, unless it is really big, but the DOM traversal.
Get the text nodes once and then work with them.
var textnodes = $x("//text()", document.body)
function $x(p, context) {
if (!context) context = document;
var i, arr = [], xpr = document.evaluate(p, context, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (i = 0; item = xpr.snapshotItem(i); i++) arr.push(item);
return arr;
}
You should see a considerable speed improvement.

Searching an array of objects using jquery

I have a log analysis script to populate a complex visualisation.
Picture an array (called, rather unoriginally, 'log') of Activity objects, each of which is in the form:
{
name:foo,
activities:[
{time:t, action:a},
{time:t, action:a},
{time:t, action:a},
...
]
}
There will be up to 75 activity objects in the array, each containing an array of 400-600 actions (one timeslot every 5 minutes since midnight the previous day).
Given a known activity name (foo above) and a time that will already exist in the activities array, I need to update the associated action.
Each name will be unique and each time is in ascending order in the array in exact 5 minutes increments.
As I have to do this a little over 1000 times every time the graph is updated (so an average of 1000 values to update and 1000*500*60 points to plot), performance is a fairly key issue...
Looping in jq is far more efficient than anything I could write so, at the moment, I have
n = "foo";
t = new Date(y,mm,d,h,m).toLocaleString() // matches a time stamp in the log
$.grep($.grep(log, function(n, i)
{
return (n.name == n)
}
)[0].activities, function(n, i)
{
return (n.time == t)
}
)[0].action = "bar";
That seems to be working, but it's taken me so long and I've had so many arguments with myself that I'm not confident.
Am I missing a better way?
I wont give you a better loop method for your problem, as any loop you come up with will relatively be no better than the last.
If you truly want a solution that will enhance performance, you should think about rearranging your object entirely. If every name of each log and time of each activities array is unique, you can change your object setup to have those values as the key of each subobject.
Using this method, you'll just be doing a key look up, no loop needed.
New LOG Object
var log =
{
unique_name : {
"activities" : {
time_1 : action_1,
time_2 : action_2,
time_3 : action_3,
etc...
}
},
unique_name_2 : {
"activities" : {
etc...
}
}
}
Now with var u_name = "foo"; and var t = "some time"; you can simply do...
log[u_name][t] = "some action";
Hope this helps!
Seems like you want the first matched activity of the first matched log.
In that case, you should break the loop after the first match is found. You can do this with .some().
n = "foo";
t = new Date(y,mm,d,h,m).toLocaleString() // matches a time stamp in the log
log.some(function(ob, i) {
if (ob.name == n) {
ob.activities.some(function(ob2, i) {
if (ob2.time == t) {
ob2.action = "bar";
return true;
}
});
return true;
}
});
Also, your n parameter was shadowing your n variable, so I changed the param to ob.
But for loops will generally be quite a bit faster than functional methods.
n = "foo";
t = new Date(y,mm,d,h,m).toLocaleString() // matches a time stamp in the log
for (var i = 0; i < log.length; i++) {
var ob = log[i];
if (ob.name == n) {
for (var j = 0; j < ob.activities.length; j++) {
var ob2 = ob.activities[j];
if (ob2.time == t) {
ob2.action = "bar";
break;
}
}
break;
}
}
If you decide that you should keep the outer loop going if there's no match found on the inner loop, change the code to one of these:
n = "foo";
t = new Date(y,mm,d,h,m).toLocaleString() // matches a time stamp in the log
log.some(function(ob, i) {
if (ob.name == n) {
return ob.activities.some(function(ob2, i) {
if (ob2.time == t) {
ob2.action = "bar";
return true;
}
});
}
});
n = "foo";
t = new Date(y,mm,d,h,m).toLocaleString() // matches a time stamp in the log
OUTER:
for (var i = 0; i < log.length; i++) {
var ob = log[i];
if (ob.name == n) {
for (var j = 0; j < ob.activities.length; j++) {
var ob2 = ob.activities[j];
if (ob2.time == t) {
ob2.action = "bar";
break OUTER;
}
}
}
}

JavaScript Associate Array access returns "literal" Array Prototype Code

Here is my code so far for my school project (using Murach's JavaScript and DOM Scripting by Ray Harris). The chapter is only about Arrays and does not cover Prototypes, but I wanted to try it out based on Internet tutorials and references:
/*
Operation
This application stores the last name, first name, and score for
one or more students and it calculates the average score for all of the scores
that have been entered. When the user clicks on the Clear button, this
application clears the score data from this application. When the user clicks
on the Sort button, this application sorts the data in alphabetical order by
last name.
Specifications
The program should use one or more arrays to store the data.
Assume that the user will enter valid data.
*/
var $ = function (id)
{
return document.getElementById(id);
}
/*
Array prototype object extension for averaging the contents
"Adding a method to the built-in Array object to extract the average
of any numerical values stored in the array is therefore a useful
addition to that object." http://javascript.about.com/library/blaravg.htm
*/
Array.prototype.average = function ()
{
var avg = 0;
var count = 0;
for (var i = 0; i<this.length; i++)
{
//never gets here:
alert(i + ": " + this[i]);
var e = +this[i];
if(!e && this[i] !== 0 && this[i] !== '0')
{
e--;
}
if (this[i] == e)
{
avg += e;
count++;
}
}
return avg / count;
}
var addScore = function ()
{
studentScores[$('last_name').value + ', ' + $('first_name').value] = $('score').value;
update();
}
var clearScore = function ()
{
for (var i in studentScores)
{
studentScores[i] = '';
}
update();
}
var sortScore = function ()
{
scores.sort();
update();
}
var update = function ()
{
var result = '';
for (var i in studentScores)
{
result += (i + ': ' + studentScores[i] + '\n');
}
$('scores').value = result;
$('average_score').value = studentScores.average().toFixed(1);
}
window.onload = function ()
{
//a variable is initialized inside a function without var, it will have a global scope:
studentScores = [];
$('add_button').onclick = addScore;
$('sort_button').onclick = sortScore;
$('clear_button').onclick = clearScore;
$('last_name').focus();
}
When the code enters the "update()" function (end of the "addScore()" function) and accesses the array,
it populates the "literal" code from the Prototype into the text area (and fails to find the average on the next line):
I don't have enough rep points to post the image, but here is my output (there are no errors in the Chrome JS Console):
lowe, doug: 82
average: function ()
{
var avg = 0;
var count = 0;
for (var i = 0; i<this.length; i++)
{
//never gets here:
alert(i + ": " + this[i]);
var e = +this[i];
if(!e && this[i] !== 0 && this[i] !== '0')
{
e--;
}
if (this[i] == e)
{
avg += e;
count++;
}
}
return avg / count;
}
Any help appreciated (best practice or algorithm suggestions welcome)
Change this:
studentScores = []
to this:
studentScores = {}
...so that you're using an Object instead of an Array.
Your for loop in average() is just iterating numeric indices instead of the non-numeric keys you created.
Create your average() method as a standalone function like the others, and pass studentScores to it to calculate the average, and then use for-in instead of for.
That's simple: Do not use for…in enumerations for looping Arrays! You do so in your clearScore and update functions.
for (var prop in obj) loops over all [enumerable] properties, including those that are inherited from Array.prototype (for Array objects at least). A for (var i=0; i<array.length; i++) loop will not have that problem.
You have to decide whether studentScores is intended to be an array (i.e., an integer is used to access the stored data) or an Object/Associative Array (a string is used to set/get an element).
If you want to use the student's name as the key, you should declare studentScores as an object, and your 'average' method would have to be added to the Object prototype (which I don't recommend).
With the current state of the code, you have stumbled on the fact that an Array is also an object, and can have arbitrary properties attached to it, like any other object. You have added properties by name, but in your average method, you are trying to access numerically based indices. But that's not where the data you're adding is stored.
> a = [];
[]
> a['foo'] = 'bar';
'bar'
> a.length
0
> a[3] = 0;
0
> a.length
4

Generating a random value, keeping a history of returned values

For a project I'm working on, I needed a Javascript function that would return a random number, in a given range, without repeating itself until the whole range is 'depleted'. As there was no such thing around, I have managed to create it myself.
The function will also require an id to be passed. This way, if you require multiple random numbers, each with their own history, the id keeps track of them all.
The function works, however I need some advice;
is this the 'proper' way to achieve what I want to achieve?
how fast will inArray() perform with very big ranges (maxNum) values? I have a feeling that large numbers will slow the function down, as it is randomizing numbers until it generates a number that is still 'valid' (i.e. not in the history array). But I can't figure out another way to do this..
The script:
var UniqueRandom = {
NumHistory: [],
generate: function (maxNum, id) {
if (!this.NumHistory[id]) this.NumHistory[id] = [];
if (maxNum >= 1) {
var current = Math.round(Math.random() * (maxNum - 1)), x = 0;
if (maxNum > 1 && this.NumHistory[id].length > 0) {
if (this.NumHistory[id].length !== maxNum) {
while ($.inArray(current, this.NumHistory[id]) !== -1) {
current = Math.round(Math.random() * (maxNum - 1));
x = x + 1;
}
this.NumHistory[id].push(current);
} else {
//reset
this.NumHistory[id] = [current];
}
} else {
//first time only
this.NumHistory[id].push(current);
}
return current;
} else {
return maxNum;
}
},
clear: function (id) {
this.NumHistory[id] = [];
}
};
usage would be: (100 being the range (0-100) and the_id being.. well, the id)
UniqueRandom.NumHistory[100, 'the_id']
I have set up a Fiddle with a demo.
It's not best practice. Imo it would be better to instantiate an object per series of numbers that needs to be generated.
I'd suggest generating an array of all possible values and shuffling it. Then you can just pop of it.
I took Jack's code and adapted it to work with the popping array method.
function fisherYates ( myArray ) {
var i = myArray.length;
if ( i == 0 ) return false;
while ( --i ) {
var j = Math.floor( Math.random() * ( i + 1 ) );
var tempi = myArray[i];
var tempj = myArray[j];
myArray[i] = tempj;
myArray[j] = tempi;
}
}
function RandomGenerator(maxNum) {
this.max = maxNum;
this.initRandomArray();
}
RandomGenerator.prototype.generate = function() {
// if no more numbers available generate new array
if( this.left === 0 ) this.initRandomArray();
this.last = this.arr.pop();
this.left = this.arr.length;
this.history.push( this.last );
return this.last;
}
RandomGenerator.prototype.initRandomArray = function() {
this.arr = [];
this.history = [];
this.last = null;
this.left = this.max;
for( var i = 0; i < this.max; i++ ) {
this.arr.push( i );
}
fisherYates( this.arr );
}
var mygen = new RandomGenerator(100);
console.log( mygen.generate() );
I got the fisherYates algorithm from here.
The approach of generating a new random number if it is already found in a history object will result in unnecessary looping.
Fiddle here
I tend to think that it is indeed not most efficient. I dont immediately get the //first time only.
Further, you can make code more readable by skipping the else return .. and writing the condition to be the opposite, e.g.:
if (maxNum >= 1) {
//code
} else {
return maxNum;
}
becomes
if (maxNum < 1) { // or maybe even if maxNum == 0
return maxNum;
}
//code
Also your x variable seems to be redundant.
I would probably implement it like this, using actual instances of random generators. This keeps the history of each generator separated.
function RandomGenerator(maxNum)
{
this.max = maxNum;
this.history = {};
this.histn = 0;
}
// generate random number in range [0..maxNum)
RandomGenerator.prototype.generate = function()
{
var value;
if (this.histn == this.max ) {
return false;
}
do {
value = Math.floor(Math.random() * this.max );
} while (this.history[value]);
this.history['' + value] = 1;
++this.histn;
return value;
}
var mygen = new RandomGenerator(100);
console.log(mygen.generate());
In my implementation I'm choosing a plain object for the history instead of an array; testing whether a value has been generated before is done by testing a property instead of $.inArray().
I agree with Alex that in most use cases, you'd want to generate an array of all values, shuffle them, and then pop them as you need them instead.
Here is an example:
var getShuffledUniqueRandoms = function(count, suffix) {
var values = [];
for (var i = 1; i < count+1; i++) {
values.push(i + suffix);
}
// Shuffle function originally from:
//+ Jonas Raoni Soares Silva
//# http://jsfromhell.com/array/shuffle [v1.0]
return (function(o){ //v1.0
for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
return o;
})(values);
}
var values = getShuffledUniqueRandoms(10, "index");
$('button').click(function() {
if (values.length == 0) {
$('body').append('<p>Out of values.</p>');
} else {
$('body').append('<p>' + values.pop() + '</p>');
}
});
​
​FIDDLE
With this algorithm, it has a bigger upfront cost, but at least it has a known time it'll take to complete (roughly O(n)).
With the algorithm where you are constantly checking to see if a random value is in an array, it'll get slower and slower with each new iteration.
Now if your data set is always relatively small, your algorithm could work a little better, but anything larger than 10 or so and it starts losing it's edge.

Categories

Resources