views:

5182

answers:

7

I have a table which has a header row, but also a header column and a total column with several columns in between.

Something like this:

Name    Score 1    Score 2  ...  Total
--------------------------------------
John       5          6            86
Will       3          7            82
Nick       7          1            74

The entire table is defined inside a fixed-width scrollable div because there are likely to be a large number of "Score" rows and I have a fixed-width page layout.

<div id="tableWrapper" style="overflow-x: auto; width: 500px;">
    <table id="scoreTable">
        ...
    </table>
</div>

What I would like is for the first (Name) and last (Total) columns to remain visible while the inner columns scroll.

Can anyone help me with this?

Edit: I mean horizontal scrolling only - changed to specify that.


Update: I've solved this problem for myself and have posted the answer below. Let me know if you need any more information - this was a bit of a pain to do and I'd hate for someone else to have to rewrite everything.

A: 

Did u mean horizontal scrolling??

If you want to achieve horizontal scrolling then you can use 3 containers.

  1. For the first column ( Name )
  2. For the columns that you want to scroll. Set the overflow-x style of this container to auto
  3. For the last column ( Total )
rahul
I think what he means is that he wants the Name and Total columns to stay in place, while Score1, Score2, etc, all scroll. This is possible in MS Excel, and I see no reason why it would be confusing to the user. However, I have no clue as to how to accomplish it.
hmcclungiii
Yes, that's what I'm after, but I'm not sure this'll accomplish it.The difficulty with putting containers around the sections (apart from invalid html) is that columns aren't consecutively defined. The <td> tag repeats for each row.
Damovisa
It could be better to stuck with your initial design itself. Because if you alter your middle columns then you have to take into consideration the height of the scroll bars too, so that every item in a single row aligns to the same line.
rahul
The div has no fixed height though - I'm happy for the height to expand as it does.
Damovisa
+2  A: 

I assume you want to scroll it horizontally only. Otherwise it could be confusing with static columns.

You could put the overflow: auto onto a containing element of the inner table cells only... I'm not sure how browsers would handle this, but you may be able to put the elements you want fixed inside thead and tfoot elements, and put the scrolling portion inside a tbody element, and set it's overflow to auto.

Failing that, you may need to drop semantics and code the left column and right column outside the table.

Personally, I'd try code it as semantic as possible, and then use JavaScript to position the left and right columns. Without JavaScript, you should make it fail gracefully to just a wide table (not sure how difficult this would be, as you say you have a fixed width)

You could try this (rough) jQuery example to put the first column's values outside.

var nameTable = '<table id="outside-name-col">
                  <thead>
                   <tr>
                    <th>Name</th>
                   </tr>
                  </thead>
                 <tbody>'; // start making a table for name column only

$('#scoreTable tbody tr').each(function() { // iterate through existing table

   var nameCol = $(this).find(':first'); // get column of index 0, i.e. first

   var cellHeight = nameCol.height(); // get the height of this cell

   $(this).find('td').height(cellHeight); // equalise the height across the row so removing this element will not collapse the height if it is taller than the scores and total       

   nameTable += '<tr><td style="height: ' + cellHeight + 'px">' + nameCol.html() + '</td></tr>'; // append the next row with new height and data

   nameCol.remove(); // remove this cell from the table now it's been placed outside

});

nameTable += '</tbody></table>'; // finish table string


$('#scoreTable').before(nameTable); // insert just before the score table

Use CSS to position them to align correctly. This is untested, but it should give you some idea.

alex
Sorry, yes - scrolling horizontally only, but it expands vertically at the moment anyway :)
Damovisa
As with my comment for phoenix's answer, I don't think I can do it with html. The <td> tags aren't defined consecutively in the html so I can't really wrap them with a div.
Damovisa
I would leverage jQuery to do it... I'd cut out the first and last columns and position them outside the table and then set the table to overflow-x: auto
alex
Cool, I think that's what I'll look at doing. Any suggestions on how to do that? :)
Damovisa
That will not work in many browsers. There are better but harder solutions.
Thanks
A jQuery solution will work in all browsers I care about :)
Damovisa
Why would you use jQuery to 'cut out' columns? If you're going to 'cut' them out, you might as well not output that data into the table in the first place. Also, columns only arise incidentally, when TDs share a common index among a series of TRs. You can't just point to a 'column' in the DOM.
ozan
I think he meant that I should not include them in the scrollable table, rather to "cut them out" of that table into their own tables either side of the scrollable one.And yes, I take your point regarding TDs sharing an index among TRs - that's why a css-only solution probably isn't possible.
Damovisa
I would output the data for a semantic and readable table... one that would still be readable without CSS or JS. I know you can not remove a column within the dom by removing one element, but jQuery has selectors to help you with this. I've updated my answer.
alex
Thanks for that update alex - I'll give it a try.
Damovisa
Okay so what happens when the height of a TR in the names table doesn't end up being the same as the corresponding TR in the original table? For instance if there's a decision to restrict the width of the names table and a long name runs over two lines?
ozan
This is where you must either enforce a fixed height or use some fancy JS. I'd just use a fixed height... judging from the example data, first names and scores, I don't see there being a problem with fixed height cells.
alex
True, until someone else working on some other part of the project decides to include a surname, or for some other reason doesn't want a fixed cell height. Would you be happy to rethink the rendering logic to accommodate such a simple change?
ozan
This could be where comments in the code would be useful.. also, could this be premature optimisation if at the current time all that is wanted to be displayed is the first name only? Could they be run through an abbreviate function so Tom Jones becomes Tom J... ? Something to consider.
alex
Yeah I guess it depends on the circumstances of the project, and how flexible the code needs to be.
ozan
Well, if the conditions (fixed height rows) satisfy the OP, then this should work fine.
alex
I'll update to take into account different height cells.
alex
To clarify, yes, the names can technically be changed to pretty much anything (50 char max). So I will have to think about row heights.
Damovisa
Thanks for this solution Alex - I've had a play with this idea and I think it's on the right track.
Damovisa
No worries, glad I could be of some help!
alex
+3  A: 

Can I propose a somewhat unorthodox solution?

What would you think about placing the 'total' column after the 'name' column, rather than at the very end? Wouldn't this avoid the requirement for only a portion of the table to scroll?

It's not exactly what you're asking for, but perhaps it is a sufficient solution, given that the alternative would be pretty messy. (Placing the 'total' and 'name' columns outside of the table, for instance, would create alignment problems when not all rows are of equal height. You could correct this with javascript but then you'd be entering a whole new world of pain).

Also from a UI perspective, it may be that 'name' and 'total' are the most important data, in which case it would make sense to put them together, followed by a sort of 'breakdown' of the total. Of course, we seem to have an intuition that a 'total' should come after its constituent parts, but I don't think it would cause too much confusion to the user if the order were reversed like this (though this is a question for you, based on your product and your users).

Anyway, something to consider.

EDIT:

Here are some more unorthodox solutions, now that I think I understand your intentions a bit better:

  1. Paginate the scores. Give the most recent ten, say, and the total, and a link to older scores, which are provided 10 at a time
  2. Only give names, totals, and some other meaningful measures such as mean and sd, then provide a link for each name that shows all results corresponding to that name. You could then also provide a link showing all results for a given score set, so that comparisons between different users can be made. The point is that you'd only have to give 1 dimension of data for each view, rather than having an unwieldy 2D data set
  3. Make the rows sortable (easy with jQuery UI) so that if I want to compare Mary to Jane, I can drag and place one after the other, so I wont need to keep scrolling left and right to see which scores correspond to which names
  4. Highlight a row when it is clicked, by changing the background color or similar, again so I don't need to keep scrolling left and right.

Anyway you get the idea. Perhaps it is better to look for a UI solution than a contorted markup solution. Ultimately I would be questioning how important it is to present so much data to the user at once, that a portion of it needs to scroll in a particular fashion for the data to be readable. Perhaps you're building a spreadsheet app, and you really do need to display a 100x100 matrix in a single view. If not, you could surely come up with more creative ways than I have to split up the results.

ozan
True - the total could be put at the start and it would make sense. It really comes down to what I want the users to see - while they browse through the other columns, it'd be good for them to be able to see the name and the total. I'd still want to only scroll the rest.
Damovisa
I guess if you want to scroll the rest anyway it doesn't make all that much of a difference. I thought your main concern was that a user might not realise that the 'total' column exists if they are required to scroll to it.
ozan
No, more that if they want to compare people's scores, they'd need to keep scrolling back and forth between the name and the later score columns or total.
Damovisa
This is a good answer, to look at it from a different perspective +1
alex
Yeah, great update. Thanks for that Ozan.
Damovisa
A: 

Scrolling the content of tables is an problematic issue, since there is no simple and browser-safe solution to achieve that.

The best and most secure way to do it requires JavaScript. You would achieve even horizontal Scrolling easily (it's included for free) with this technique I used. You can transform that to your problem easily:

1) I made 3 Tables

2) Placed the middle Table that holds the Data inside a DIV container

3) set the DIV style to overflow:auto

4) gave the header and footer table and the div each a width of 100%

5) surrounded everything by another div to control the overall table width

6) made very sure that the quantity of columns match in all three tables, and that all three have the same styles regarding paddings, margins and borders

now the interesting part:

6) below the last element of that table construction, write a javascript block (or include it from a file)

7) write a function that successively looks at each column (use a for loop), starting with the first one. At every iteration of that loop, check the td cell with of the first two tables in the current column. Check which with is the bigger one and assign that to the smaller td cell's style. You may want to apply that value to the footer table, too.

8) call that function. You may want to do that in an interval or when special events occur, such as new data in the cells (if using ajax for example).

Note: If you do scroll horizontally, your columns sizes are synchronized. But you will have to synchronize the scrolling position of your header and footer table with the content table. Alternatively you just let the overall div scroll the whole thing horizontally, but not vertically. That would be the job of the DIV that contains the data table.

This technique worked very well in all major browsers in an very coplex gantt diagram project.

Note: You can think of that concept easily in a 90 degrees rotated way, so that you fix the first and last header column when scrolling.

Edit: Here is another example from my code base that might help you to get the solution:

var left_td = document.getElementById("left_td");
var right_td = document.getElementById("right_td");
var contentDiv = document.getElementById("contentDiv");
window.setInterval(function(){
    var left_td_width = Math.round((parseInt(contentDIV.offsetWidth) - 120) / 2);
    var right_td_width = (parseInt(contentDIV.offsetWidth) - left_td_width - 120;

    left_td.style.width = left_td_width + "px";
    right_td.style.width = right_td_width + "px";
}, 25);

This code does the following: It makes sure, that an Table with 3 columns always looks like that: The first and last column have the same size, and the center column has a width of 120 pixels. Sure that does not apply directly to your special problem, but It might give you a starting point in dynamic table manipulation through JavaScript.

Thanks
Correct me if I'm wrong, but this seems to be a solution for a freezing a header row and footer row rather than a header column?
Damovisa
This is precisely the 'whole new world of pain' that I was referring to in my answer.
ozan
Ok... I don't want to freeze rows though, only columns. I'm starting to see that there's a world of pain involved :)
Damovisa
sorry I've got confused by rows and columns, but I've updated the description a little bit. Well, it's not really painful. It's hard to start, but it's fun doing it.
Thanks
+2  A: 

It's not complete right now (I just mocked it up really quick), but here's a way of doing it using straight HTML. You want three tables ... two outside the <div> and one inside the <div>. All three are floated to the left, so that they're all on the same line.

Table Test

and the code itself:

  <table style="float: left;">
     <tr>
        <td>Name</td>
     </tr>
     <tr>
        <td>John</td>
     </tr>
     <tr>
        <td>Will</td>
     </tr>
  </table>
  <div id="tableWrapper" style="overflow-x: auto; width: 500px;float:left;padding-bottom: 10px;">
      <table>
        <tr>
           <td>Score1</td>
           <td>Score2</td>
           ...
        </tr>
        <tr>
           <td>5</td>
           <td>6</td>
           ...
        </tr>
        <tr>
           <td>3</td>
           <td>7</td>
           ...
        </tr>
        <tr>
           <td>7</td>
           <td>1</td>
           ...
        </tr>
      </table>
  </div>
  <table style="float: left;">
     <tr>
        <td>Total</td>
     </tr>
     <tr>
        <td>86</td>
     </tr>
     <tr>
        <td>82</td>
     </tr>
  </table>

Note: The padding-bottom on the div is so that the scrollbar does not cover up the table in IE. Also, the bug I have to work out is how to specify the width of the header elements inside the middle table. For some reason, specifying the width="" attribute does not work. Thus, if the text of the header element is too wide (and breaks onto another line), then the layout is broken (off by one row)

cmptrgeekken
as I said, that's impossible to achieve over all major browsers without JavaScript. You may want to synchronize all height values of all rows in a loop, and you may want to do that again over events or in an interval if things change. another option would be position absolute and fixed with in div.
Thanks
True - good answer though and something to consider if I can control the header height somehow...
Damovisa
+1  A: 

Hi all,

I've experimented with a few methods (thanks to everyone who helped) and here's what I've come up with using jQuery. It seems to work well in all browsers I tested. Feel free to take it and use it however you wish. Next step for me will be turning it into a reusable jQuery plugin.

Summary:

I started with a normal table with everything in it (Id="ladderTable"), and I wrote Three methods - one to strip the first column, one to strip the last column, and one to fix the row heights.

The stripFirstColumn method creates a new table (Id="nameTable"), traverses the original table and takes out the first column, and adds those cells to the nameTable.

The stripLastColumn method does basically the same thing, except it takes out the last column and adds the cells to a new table called totalTable.

The fixHeights method looks at each row in each table, calculates the maximum height, and applies it to the related tables.

In the document ready event, I called all three methods in order. Note that all three tables float left so they'll just stack horizontally.

The HTML Structure:

<h1>Current Ladder</h1> 
<div id="nameTableSpan" style="float:left;width:100px;border-right:2px solid gray;"></div> 
<div id="ladderDiv" style="float:left;width:423px;overflow:auto;border:1px solid gray;margin-top:-1px;"> 
    <table id="ladderTable" class="ladderTable">
     <thead> 
            <tr><td>Name</td><td>Round 1</td> ... <td>Round 50</td><td class="scoreTotal">Total</td></tr>
     </thead>
  <tr><td>Bob</td><td>11</td> ... <td>75</td><td>421</td></tr>
     ... (more scores)
 </table>
</div>
<div id="totalTableSpan" style="float:left;width:70px;border-left:2px solid gray;"></div>

The jQuery:

function stripFirstColumn() {                
    // pull out first column:
    var nt = $('<table id="nameTable" cellpadding="3" cellspacing="0" style="width:100px;"></table>');
    $('#ladderTable tr').each(function(i)
    {
        nt.append('<tr><td style="color:'+$(this).children('td:first').css('color')+'">'+$(this).children('td:first').html()+'</td></tr>');
    });
    nt.appendTo('#nameTableSpan');
    // remove original first column
    $('#ladderTable tr').each(function(i)
    {
        $(this).children('td:first').remove();
    });
    $('#nameTable td:first').css('background-color','#8DB4B7');
}

function stripLastColumn() {                
    // pull out last column:
    var nt = $('<table id="totalTable" cellpadding="3" cellspacing="0" style="width:70px;"></table>');
    $('#ladderTable tr').each(function(i)
    {
        nt.append('<tr><td style="color:'+$(this).children('td:last').css('color')+'">'+$(this).children('td:last').html()+'</td></tr>');
    });
    nt.appendTo('#totalTableSpan');
    // remove original last column
    $('#ladderTable tr').each(function(i)
    {
        $(this).children('td:last').remove();
    });
    $('#totalTable td:first').css('background-color','#8DB4B7');
}

function fixHeights() {
    // change heights:
    var curRow = 1;
    $('#ladderTable tr').each(function(i){
        // get heights
        var c1 = $('#nameTable tr:nth-child('+curRow+')').height();    // column 1
        var c2 = $(this).height();    // column 2
        var c3 = $('#totalTable tr:nth-child('+curRow+')').height();    // column 3
        var maxHeight = Math.max(c1, Math.max(c2, c3));

        //$('#log').append('Row '+curRow+' c1=' + c1 +' c2=' + c2 +' c3=' + c3 +'  max height = '+maxHeight+'<br/>');

        // set heights
        //$('#nameTable tr:nth-child('+curRow+')').height(maxHeight);
        $('#nameTable tr:nth-child('+curRow+') td:first').height(maxHeight);
        //$('#log').append('NameTable: '+$('#nameTable tr:nth-child('+curRow+')').height()+'<br/>');
        //$(this).height(maxHeight);
        $(this).children('td:first').height(maxHeight);
        //$('#log').append('MainTable: '+$(this).height()+'<br/>');
        //$('#totalTable tr:nth-child('+curRow+')').height(maxHeight);
        $('#totalTable tr:nth-child('+curRow+') td:first').height(maxHeight);
        //$('#log').append('TotalTable: '+$('#totalTable tr:nth-child('+curRow+')').height()+'<br/>');

        curRow++;
    });

    if ($.browser.msie)
        $('#ladderDiv').height($('#ladderDiv').height()+18);
}

$(document).ready(function() {
    stripFirstColumn();
    stripLastColumn();
    fixHeights();
    $("#ladderDiv").attr('scrollLeft', $("#ladderDiv").attr('scrollWidth'));    // scroll to the last round
});

If you have any questions or if there's anything that wasn't clear, I'm more than happy to help.

It took me quite a while to work out that there was nothing that I could really reuse and it took a bit longer to write this. I'd hate for someone to go to the same trouble.

Damovisa
A: 

Hi Damovisa,

Your script is a helpful start to something I am working on at my job to develop a plugin for tables. I am combining it with some other features. If it comes out any good I would like to publish the plugin and give you an attribution credit.

jasongonzales
Hi Jason,Sounds great to me! Let me know if you release anything, I'd love to see it :)
Damovisa