patternjavascriptMinor
Large table mousehover effects
Viewed 0 times
effectslargemousehovertable
Problem
HTML
Where
JQUERY/JS
(I know that I could have written a recursive function as well, that's not the point right now)
When someone mouse-overs a TR, I want that row to be one color, all the rows that has the hovered row as parent, another color, the grandchildren another color etc (for several levels deep).
Now, for 100 rows, this isn't a problem, but my data contains around 30 000 rows, and that's where it starts to go wrong, several-seconds-of-freezing-wrong.
How can I optimise this code?
jsFiddle
Where
data-parent points to the data-id of another row.JQUERY/JS
$("tr[data-parent='" + id + "']").css("background-color", "#1ba1e2").each(function () {
$("tr[data-parent='" + $(this).attr("data-id") + "']").css("background-color", "#75F7FF").each(function () {
$("tr[data-parent='" + $(this).attr("data-id") + "']").css("background-color", "#98E8E3").each(function () {
$("tr[data-parent='" + $(this).attr("data-id") + "']").css("background-color", "#B6DCE8")
});});});(I know that I could have written a recursive function as well, that's not the point right now)
When someone mouse-overs a TR, I want that row to be one color, all the rows that has the hovered row as parent, another color, the grandchildren another color etc (for several levels deep).
Now, for 100 rows, this isn't a problem, but my data contains around 30 000 rows, and that's where it starts to go wrong, several-seconds-of-freezing-wrong.
How can I optimise this code?
jsFiddle
Solution
The problem with the current approach, as I'm sure you already know, is the very high number of DOM elements that the JavaScript engine is navigating through and assessing before the end of the process (bearing the 30,000 row scenario in mind).
And the jQuery selector function has a fair bit of work to do, checking every row element to see if it has the data attribute value or not, before it can even begin the highlighting. A taxing process. DOM manipulation is notoriously hard on performance.
My answer would be that the mapping between row and parent, which is what essentially needs to be calculated for each row when hovered, needs to be taken out of the DOM entirely so this calculation doesn't require any work with the DOM at all.
See my solution below, and this JSFiddle: https://jsfiddle.net/dawdg4sj/4
Ultimately, you'll see that on each row hover, the 'calculateHighlighting' function is called which performs the calculation "off-line" so to speak, away from the DOM (data attributes withdrawn) and instead uses the local 'map' object (row IDs on the left, parent row IDs on the right), and returned is a straight-forward array of objects each describing an element by ID and a class to add for highlighting purposes.
Bottom line, you need to be able to generate the row id/parent mapping outside the DOM and inside JavaScript instead. As long as you have this mapping information inside the DOM and wish to process 30,000 rows, it'll be a sluggish affair.
(Note: My solution uses function calls from the Lodash library for convenience, i.e. '.keys(.pick(...'. They are optional, keep or swap.)
Finally, and separately, I would express my surprise at such a large number of rows. Working with so many elements in a single view of an app via JavaScript is always going to lead to slow processing to one extent or another. But obviously so many rows could not be visible within the viewport all at once, so I would further suggest that part of the solution is to limit the highlighting to those within view and just outside only, and update on scroll, or even paginate the rows. It depends on the context of the project of course, but those are my supplementary thoughts.)
And the jQuery selector function has a fair bit of work to do, checking every row element to see if it has the data attribute value or not, before it can even begin the highlighting. A taxing process. DOM manipulation is notoriously hard on performance.
My answer would be that the mapping between row and parent, which is what essentially needs to be calculated for each row when hovered, needs to be taken out of the DOM entirely so this calculation doesn't require any work with the DOM at all.
See my solution below, and this JSFiddle: https://jsfiddle.net/dawdg4sj/4
Ultimately, you'll see that on each row hover, the 'calculateHighlighting' function is called which performs the calculation "off-line" so to speak, away from the DOM (data attributes withdrawn) and instead uses the local 'map' object (row IDs on the left, parent row IDs on the right), and returned is a straight-forward array of objects each describing an element by ID and a class to add for highlighting purposes.
Bottom line, you need to be able to generate the row id/parent mapping outside the DOM and inside JavaScript instead. As long as you have this mapping information inside the DOM and wish to process 30,000 rows, it'll be a sluggish affair.
(Note: My solution uses function calls from the Lodash library for convenience, i.e. '.keys(.pick(...'. They are optional, keep or swap.)
Finally, and separately, I would express my surprise at such a large number of rows. Working with so many elements in a single view of an app via JavaScript is always going to lead to slow processing to one extent or another. But obviously so many rows could not be visible within the viewport all at once, so I would further suggest that part of the solution is to limit the highlighting to those within view and just outside only, and update on scroll, or even paginate the rows. It depends on the context of the project of course, but those are my supplementary thoughts.)
// ---
// New-look HTML
// ---
test
tes
tes
test
tes
tes
test
// ---
// New-look JS
// ---
var map = { // row id -> parent row id
1: 0,
2: 1,
3: 1,
4: 3,
5: 3,
6: 1,
7: 0
};
var highlightColours = 4;
// calculateHighlighting
// Description: Establish a straight-forward array of highlighting rules (classes to add to elements)
// e.g. [ {element: '#row-1', class: 'row-colour-1'}, {element: '#row-2', class: 'row-colour-4'} ... ]
function calculateHighlighting(rootElementID) {
var result = [];
var elementIDPrefix = '#row-';
var elementColourClassPrefix = 'row-colour-';
var parentRows = [];
var id = rootElementID.split('-')[1]; // (e.g. '#row-1' -> 1)
parentRows.push(id); // Start with the root (hovered) element
var colourIndex = 1; // Keep track of highlight colours used
while(parentRows.length > 0 || colourIndex 0) { // Highlight found parent rows
for(var j = 0; j < childRows.length; j++) {
result.push({
element: elementIDPrefix + childRows[j],
class: elementColourClassPrefix + colourIndex
});
}
}
}
parentRows = childRows; // Just highlighted parent rows, become base for next parent search
colourIndex++; // Next highlight colour
}
return result;
}
$('tr').on('mouseover', function () {
var rowID = $(this).attr('id');
var rowsToHighlight = calculateHighlighting(rowID);
for(var i = 0; i < rowsToHighlight.length; i++) {
$(rowsToHighlight[i].element).addClass(rowsToHighlight[i].class);
}
});
$('tr').on('mouseleave', function () {
$('tr').attr('class', '');
});
// ---
// New-look CSS
// ---
tr:hover {
background-color: red;
}
.row-colour-1 {
background-color: #1ba1e2;
}
.row-colour-2 {
background-color: #75F7FF;
}
.row-colour-3 {
background-color: #98E8E3;
}
.row-colour-4 {
background-color: #B6DCE8;
}Code Snippets
// ---
// New-look HTML
// ---
<table>
<tr id="row-1">
<td>test</td>
</tr>
<tr id="row-2">
<td>tes</td>
</tr>
<tr id="row-3">
<td>tes</td>
</tr>
<tr id="row-4">
<td>test</td>
</tr>
<tr id="row-5">
<td>tes</td>
</tr>
<tr id="row-6">
<td>tes</td>
</tr>
<tr id="row-7">
<td>test</td>
</tr>
</table>
// ---
// New-look JS
// ---
var map = { // row id -> parent row id
1: 0,
2: 1,
3: 1,
4: 3,
5: 3,
6: 1,
7: 0
};
var highlightColours = 4;
// calculateHighlighting
// Description: Establish a straight-forward array of highlighting rules (classes to add to elements)
// e.g. [ {element: '#row-1', class: 'row-colour-1'}, {element: '#row-2', class: 'row-colour-4'} ... ]
function calculateHighlighting(rootElementID) {
var result = [];
var elementIDPrefix = '#row-';
var elementColourClassPrefix = 'row-colour-';
var parentRows = [];
var id = rootElementID.split('-')[1]; // (e.g. '#row-1' -> 1)
parentRows.push(id); // Start with the root (hovered) element
var colourIndex = 1; // Keep track of highlight colours used
while(parentRows.length > 0 || colourIndex <= highlightColours) { // While there are parent rows to highlight (or we run out of highlight colours)
var childRows;
for(var i = 0; i < parentRows.length; i++) {
// Fetch related rows based on id/parent mapping (using lodash function, _.keys, for convenience)
childRows = _.keys(_.pick(map, function(parentID) {
return parentID == parentRows[i];
}));
if(childRows.length > 0) { // Highlight found parent rows
for(var j = 0; j < childRows.length; j++) {
result.push({
element: elementIDPrefix + childRows[j],
class: elementColourClassPrefix + colourIndex
});
}
}
}
parentRows = childRows; // Just highlighted parent rows, become base for next parent search
colourIndex++; // Next highlight colour
}
return result;
}
$('tr').on('mouseover', function () {
var rowID = $(this).attr('id');
var rowsToHighlight = calculateHighlighting(rowID);
for(var i = 0; i < rowsToHighlight.length; i++) {
$(rowsToHighlight[i].element).addClass(rowsToHighlight[i].class);
}
});
$('tr').on('mouseleave', function () {
$('tr').attr('class', '');
});
// ---
// New-look CSS
// ---
tr:hover {
background-color: red;
}
.row-colour-1 {
background-color: #1ba1e2;
}
.row-colour-2 {
background-color: #75F7FF;
}
.row-colour-3 {
background-color: #98E8E3;
}
.row-colour-4 {
background-color: #B6DCE8;
}Context
StackExchange Code Review Q#86968, answer score: 4
Revisions (0)
No revisions yet.