I was using angular-ui-grid (http://ui-grid.info/) to display tabular data. On the whole, it was quite slow and so we decided to use ag-grid (https://www.ag-grid.com/). This was much more performant and better to deal with for regular-sized data-sets.
However, now we are working with some tabular data of the size of 100 cols x 10,000 rows (~1M cells) and the grid seems quite slow in performance.
I was wondering if anyone had used hypergrid (https://fin-hypergrid.github.io/core/2.0.2/) -- it seems to 'solve' the issue of large cols x large rows and in their demo it seems much faster (almost by an order of magnitude) on large data sets.
How does hypergrid compare to ag-grid or react-virtualized in performance on large data sizes?
I haven't tried any of those example libraries you mentioned, but perhaps I could explain why fin-hypergrid stands out the most. My opinion is primarily based on my JavaScript knowledge and how this kind of stuff works in the back.
I should probably start with react-virtualized and ag-grid:
Both use the way of populating the DOM and only displaying a portion of data to the view, dynamically removing things from the DOM that aren't visible anymore and adding the upcoming things in advance. Now the problem here lies in adding and removing things in the DOM since this tends to be executed really fast / multiple times a second. Because of this we experience some lag or jitter. You can actually check Web Console > Profiles > Record JavaScript CPU Profiles and see that this method takes time to complete. So the only thing that differs from react-virtualized and ag-grid are their algorithms of applying these changes in the smoothest possible manner.
ag-grid, from what I can see, is the one that suffers the most from this issue as you could actually see some elements that haven't finished rendering yet and experience severe lag when you scroll too fast.
react-virtualized on the other hand does a splendid job implementing its algorithm in the smoothest possible manner. This might be the best library available in the DOM manipulation category though it still suffers from the problem of manipulating the DOM too fast which creates lag, though this is only seen when large chunks of data are involved.
Here are the reasons why fin-hypergrid excels:
The best asset of fin-hypergrid is it doesn't perform DOM manipulation at all so you are already saved from the problem caused by adding and removing things too fast since it uses <canvas>
fin-hypergrid also displays only data that the user sees and dynamically removes things that aren't visible. It also adds in advance to achieve a smooth scroll feeling, so no still-rendering items are shown.
fin-hypergrid also does a really great job on their scrolling algorithm to attain the smoothest possible manner, so there is no jitter or lag.
Now that doesn't mean that hypergrid is all good, it has some disadvantages too:
Since fin-hypergrid is made with HTML5 Canvas, styling it will become a real pain as it doesn't accept CSS. You would need to style it manually.
A few things to keep in mind are form controls such as <select>, radio buttons, checkboxes, etc. would be a real pain to implement. If you are trying to implement something like this then proceed with caution.
It's primarily used for displaying data with simple column edits, not involving anything other than a textbox, and achieving the most smooth scroll feeling.
Now in conclusion, I would probably suggest using react-virtualized instead since it offers the smoothest scroll, above fin-hypergrid. If you are willing to disregard the downsides of fin-hypergrid, then fin-hypergrid is the best option.
UPDATED:
Since we discussed JS / CSS, canvas implementations of these tables. I should have mentioned the last possible contender though this one is not primarily a js table library but a framework in which Google Sheets might have been used it is called d3.js.
d3.js has the speed and power of canvas and at the same time retains HTML structure, this means that it is possible to style it with CSS!
It maximizes the usage of HTML 5 SVG
I can't say anything more better in d3.js
The only downsides of d3.js in this discussion is that:
There is no available good table libraries out there that uses d3.js. Google Sheets that is. But they do not share code.
d3.js is just very hard to learn, though there are lots of stuff out there that helps us learn this faster but not that fast.
If you wanted speed of Canvas with CSS styling capabalities then d3.js is the key the problem is learning it.
I have gone through different data grid options. Then I found this.
Hope this answer helps to anyone who is looking for performance comparison between data grids.
Few points to be noted here, even you have gone through with the article I provided.
1 - Once a grid is 'fast enough', which means the rendering lag is not
noticeable, then it doesn't matter which grid is faster than the next.
2 - A canvas based grid is not a HTML grid - you cannot customise it
with HTML. The grid will struggle to be styled / themed / customised
by the standard JavaScript / CSS developer.
Pick your poison because it is not just the performance when it comes to the consumer level.
Have you considered using something designed for large data sets?
Clusterize.js
I believe the way it works is it only loads the elements data for the ones you are looking at. Therefore the browser doesn't lag because it has the elements need to display the viewport.
The demo page loads 3 examples with 500,000 elements each (1,500,000 total elements).
Update - With Example Snippet
Because I do not have 100,000 x 200 elements of data to load I build 100 x 200 using JavaScript.
Then I copy that array and insert it into the data array 1000 times.
This way I can reach your total data set size without overloading the JavaScript engine.
Since it is hard to tell that it is really doing 100,000 rows I called the getRowsAmount() function which is displayed at the top of the output.
You may need to play with the block & cluster sizes based on your viewport but this should show you that this library is totally possible of supporting your needs.
$(function() {
var dataBlock = []
var data = [];
const rows = 100000
const cols = 200;
const blockSize = 100;
const blocks = rows / blockSize;
for (let i = 0; i < cols; i++) {
$("thead tr").append(`<th>C${i}</td>`);
}
for (let j = 0; j < blockSize ; j++) {
var tr = $("<tr />");
for (var i = 0; i < cols; i++) {
tr.append(`<td>R${j}-C${i}</td>`);
}
dataBlock.push($("<div />").append(tr).html());
}
for (let i = 0; i < blocks; i++) {
$.merge(data, dataBlock);
}
var clusterize = new Clusterize({
rows: data,
scrollId: 'scrollArea',
contentId: 'contentArea',
rows_in_block: 10,
blocks_in_cluster: 2,
});
$("#totalRows").text(clusterize.getRowsAmount());
});
table td {
white-space: nowrap;
padding: 0 5px;
}
<html>
<head>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://clusterize.js.org/css/clusterize.css" rel="stylesheet" />
<script src="https://clusterize.js.org/js/clusterize.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
Total Rows: <span id="totalRows"></span>
<div class="clusterize">
<div id="scrollArea" class="clusterize-scroll">
<table class="table">
<thead>
<tr></tr>
</thead>
<tbody id="contentArea" class="clusterize-content">
<tr class="clusterize-no-data">
<td>Loading data…</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
The library supports appending data so with your large data sets your may want to load some of your data through AJAX.
I used free version of handsontable for big datasets.
See example with 10000*100 cells - http://jsfiddle.net/handsoncode/Lp4qn55v/
For example, for angular 1.5:
<hot-table class="hot handsontable htRowHeaders htColumnHeaders" settings="settings"
row-headers="rowHeaders" datarows="dba">
<hot-column ng-repeat="column in Value" data="{{column.COL1}}" >
</hot-column>
</hot-table>
See documentation here
Related
Introduction
I am making a request to a backend and getting a list of objects in JSON. Then I change it into an HTML table (it is a Vuetify data-table) where every object is a row.
Each row contains an array of exactly 72 ones and zeros ([1, 1, 1, 1, 0, ...]). They indicate activity back in time (72 hours).
In each row I have a for loop (kind of. It is Vue.js v-for directive) that goes through that array and loads an image 1.svg or 0.svg accordingly, to make a chart.
With 40 rows only, the table becomes to lag a little. Now, the table is quite wide and so it goes off screen (overflow: scroll or whatever).
TL;DR
Is it possible in JavaScript to somehow fluently hide DOM elements (in this case table cells (and rows)) (hide means remove from DOM, so that the browser doesn't have to render all of them) when they are off the screen?
Is there a literature you could recommend? Any tutorials? What to look for?
I remember seeing a Google talk on a list of thousands of elements scrolling smoothly on mobile, but can't seem to find it.
Demo
https://codepen.io/DCzajkowski/pen/yPbPqy
Hi Czajkowski Dariusz,
Yes it is certainly possible to fluidly handle large amounts of DOM nodes. The trick is to go one step past "hiding" the DOM nodes and to instead not render them at all.
A simplified view of this process could be broken into these steps:
Measure the size of the area in which you are rendering these nodes. -> RenderingHeight
Measure the size of one visible DOM node -> NodeHeight
Render only as many DOM nodes as will fit in the total plus a couple of extra which are used as a buffer -> ( RenderingHeight / NodeHeight) + 2 = NumberOfNodes
Create a subset of your data that should be populating these DOM nodes with values
When an action on the page occurs (scroll, click, etc..) update the subset of your data that should be displayed according to the action. Re-render the visible nodes with this new set of data.
Example: If your render a list of height 1000px and each list item will have a height of 100px and you have 1000 data points.
RenderingHeight = 1000px
NodeHeight = 100px
NumberOfNodes = 12
Render ten list item nodes using DataPoints[0] - DataPoints[11]. When a scroll event has moved the container down at least 100px you should update your selected subset of data to be DataPoints1 - DataPoints[12]. Then rather than delete nodes or append new ones, just update the data in the existing 12 nodes to use this new subset of data points.
This explanation is definitely a simplification of what you will end up doing in practice in your own applications but I hope it conveys the basic idea.
I believe the talk that you are thinking of might be this one from Google I/O this year: https://www.youtube.com/watch?v=mmq-KVeO-uU. Start at timestamp 15:45 to catch the example.
In this talk he uses the react-virtualized library as an example. Since you are working in Vue.js this library won't directly solve your problem but reading through the code might provide insight into how you might achieve this. I have used this library a couple of times and it has worked well for me.
A quick google search for virtualized lists in js yields some other vanilla js implementations that might also be helpful.
This is not a trivial mechanism which you are aiming to implement but it is amazingly powerful for managing performance when rendering large amounts of data.
Best of luck!
Example of Vue application with Polymer element canvas-datagrid with 5000 rows dataset.
new Vue({
el: '#app',
data: {
data: []
},
async created () {
var response = await fetch('https://jsonplaceholder.typicode.com/photos')
this.data = await response.json()
}
})
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
}
<div id="app">
<canvas-datagrid>
{{ data }}
</canvas-datagrid>
</div>
<script src="https://unpkg.com/vue#2.5.3/dist/vue.min.js"></script>
<script src="https://unpkg.com/canvas-datagrid#0.18.12/dist/canvas-datagrid.js"></script>
I built up a tree table structure for AngularJS (with Angular Material) some time ago.
My target was to make it work on large screens only (1280 and higher) but right now I want to update it and make it work on smaller devices (mostly tablets) without limiting data. Because of performance, I want to keep HTML as simple as possible (tree table can have 1000+ rows so creating more complicated HTML for the single row will elongate the time needed to append and render table row (rows are dynamic so it's not only about initial rendering)).
I came up with idea that I will keep the "fixed" part with the first cell which contains a name and scroll the second part which contains all metrics and will be scrolled synchronically.
Current HTML of single row:
<div class="tb-header layout-row">
<div class="tb-title"><span>Name</span></div>
<div class="tb-metrics">
<div class="layout-row">
<div class="tb-cell flex-10">812</div>
<div class="tb-cell flex-7">621</div>
<div class="tb-cell flex-4">76.5</div>
<div class="tb-cell flex-7">289</div>
<div class="tb-cell flex-4">46.5</div>
<div class="tb-cell flex-7">308</div>
<div class="tb-cell flex-4">49.6</div>
<div class="tb-cell flex-7">390</div>
<div class="tb-cell flex-4">48.0</div>
<div class="tb-cell flex-7">190</div>
<div class="tb-cell flex-4">23.4</div>
<div class="tb-cell flex-7">0</div>
<div class="tb-cell flex-4">0.0</div>
<div class="tb-cell flex-8">6.4</div>
<div class="tb-cell flex-8">0.0</div>
<div class="tb-cell flex-8"></div>
</div>
</div>
My idea was to use touchmove event on parent container (wrapping the whole tree and bind as a directive) and check when touchmove starts over the metrics section then calculate the value which I should move metrics. And that part works fine.
The problem starts when I want to apply the offset on the .tb-metrics > .
My first try was to use jQuery:
function moveMetrics( offset ) {
var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0;
$('.tb-metrics').children().css('transform', 'translateX(' + ofx + 'px)');
/*...*/
}
Unfortunately, this solution is quite slow when the table contains more rows (I cannot cache rows because they are dynamic).
In my second attempt, a tried to avoid as much DOM manipulation as I can.
To achieve that I decided to add <script> tag to dom which contains css which applies to .metrics > .layout-row.
Solution:
function moveMetrics( offset ) {
var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0
, style = $( '#tbMetricsVirtualScroll' )
;
if ( !style.length ) {
body.append( '<style id="tbMetricsVirtualScroll">.tb-metrics > * {transform: translateX(' + ofx + 'px)}</style>' );
style = $( '#tbMetricsVirtualScroll' );
} else {
style.text( '.tb-metrics > * {transform: translateX(' + ofx + 'px)}' );
}
/*...*/
}
However, it doesn't seem to be much faster when the table contains a large number of rows. So it's not DOM manipulation but rendering/painting view seems to be the bottleneck here.
I tried to create some kind of virtual scroll but because tree structure is different for different sets of data and can have an "infinite" number of levels (each row can contain children rows in new ng-repeat) it's a really hard task.
I will appreciate any ideas about how I can improve performance in that situation without using the virtual scroll.
EDIT:
Screenshot of the Chrome timeline shows that most time of scrolling is consumed by rendering (I guess that it is because of complicated DOM structure)
EDIT 2:
I won't say that I achieved absolutely smooth scrolling, but I found a couple of things for significant performance improvement (some of them weren't obvious and the result is better than I expected after such small changes).
Simplify class selectors :
.tb-header > .tb-metrics > .tb-cell is much slower than .tb-specific-cell (it seems that it take more time to parse more complicated selectors?)
remove opacity and box shadows from transformed elements
try to distribute transformed element to new layer (use css will-change and/or translateZ(0))
I have had similar problems in the past with really big tables. So, leaving the UX side of things outside of the equation - in order to achieve what you are aiming for could be the following. Instead of keeping the first tables fixed and moving all the rest, maybe you could try the other way around. Aka, keep the table inside an overflown container to allow horizontal scrolling and move using translateY the first column table cells when scrolling vertically.
In the way I am describing, you can retrieve the max width of the first table cell (or even better, set a fixed width), absolutely position each cell on the left - careful though, the relative container should be OUTSIDE the overflown table, set a padding-left on the second table cell of each row, equal to the first table-cell and then, when scrolling vertically, add a negative translateY value. The selector then should be easy to access '.layout-row .table-cell:first-child'.
Also for minor optimizing, instead of using .css() try using .attr('style', value) instead. Also, I read that you have dynamic content and can't cache it, BUT, maybe you should consider caching once every time you manipulate the DOM. Cached selected elements are still better than calculating the selector in every scroll/touchmove event.
Last but not least, you could also add somekind of debouncing technique so that the scrolling does not occur on every scroll frame, but maybe once every 10 frames - it would still be invisible to the eye, or in any case better than bottle-necking the browser with all these repaint events.
I would suggest you use lazy loading.
please note that you have 4 options in lazy loading:
There are four common ways of implementing the lazy load design pattern: lazy initialization; a virtual proxy; a ghost, and a value holder. Each has its own advantages and disadvantages (link).
Good luck
So I'm no expert myself but I do have some ideas that I think are worth mentioning.
If I'm understanding correctly, the problem is that this project needs an increasing number of HTML elements, which would continually decrease performance
Here are 3 js concepts that I think would help you
*Dynamically creating the html element:
let div = document.createElement('div');
*Dynamically creating the attributes you're going to use for the elements, and dynamically being able to assign such attributes
function attributeSetter(atr){
div.setAttribute('class', atr)
}
That way you can call the function wherever you need it, and give it whatever class
name you want to give it dynamically. Furthermore, you can use the div element as many times as you want
final concept. You can also dynamically add pre-made classes, assuming that they're already pre-defined in css, like this.
div.classList.add("my-class-name")
One more concept that might come in handy, is de-structuring
let myClasses = ['flex-10', 'flex-7', 'flex-4', 'flex-9'] //etc
let [flex10, flex7, flex4, flex9] = myClasses;
This will turn all those strings into variables that you can easily iterate through and dynamically add them to your attributeSetter function, like this:
attributeSetter(flex10)
Wherever you need it. I don't know if that helps, or if that answered your question, but at least some ideas. Good luck :)
Please take a look at this jsFiddle and press the only button to fill the list up. As you can see DIV elements inside the list are resized to fill the parent contained. This code does exactly what I want to do, but I think the way I implement this is too complex for such a seemingly simple task. Here is the code for the algorithm to assign height to inner elements:
fill = function() {
//Loop through all elements once to get total weight
var totalWeight = 0;
var totalHeight = $("#container").height() - 15; //need a little extra empty space at the buttom
$(".list").each(function(i) {
totalWeight += parseInt($(this).attr('weight'));
totalHeight -= parseInt($(this).css('margin'));
});
//Loop though the element a second time to set the relative height
$(".list").each(function(i) {
var element = $(this);
element.css("height", (element.attr("weight") / totalWeight) * totalHeight);
});
}
My question is, is the best we can do? Are there any other - hopefully faster or more elegant -- ways to achieve this functionality with reasonable cross-browser compatibility? I need to support newer Firefox, Chrome, and Safari only. I was reading about flex-box here and looking at its promising specs, but it does not look like flex-box can do weighted flexible layout consistently across browsers. If I am wrong, please show me a simple example of how this could be achieved.
This type of weighted flexible linear layout is not uncommon, for example it is available in Android SDK as one of the layout options. What is the recommended way to resize elements to fill their parent container, relative to a weight value assigned to them? A pure CSS solution would be wonderful, if at all possible.
Just a quick look over it, after about 30 mins googling. I may do a demo but don't really hav much time.
Have you looked here
html5rocks
coding.smashmag...
tutsplus
umarr
w3c
benfrain
There are some good examples on the 6th one
edit:
I was looking thought the firefox developer section on their website and found
developer.mozilla...
I also found another example with a download!!
github..
this might give you some direction for firefox and the rest should be in the other links I have provided
I am trying to create a grid like system with div's with content floated left. The problem is that the height of each div is not static since the content is different in each one. Since the height is different.. things get outta whack if one is taller then another in one row.
Is there some sort of css way or js (prefer javascript) way to get around this. I really want to avoid setting large heights on the divs to get them to line up.
You can see the example here: http://www.foreclosurejournalinc.com/index.php?option=com_hotproperty&view=type&id=1
First off - it is not correct to use a table in this situation: The table structure defines a relationship between data in the same row, and data in the same column. Since the second column is the same type of data as the first, there's no table relationship here.
You shouldn't use a table because you're defining your layout in markup, so you can only ever have two columns. For example, if the client decides they want three columns, or you want to show a single-column list for a mobile view or print stylesheet, you need to change the markup (or even have several sets of markup for different purposes).
Flexible markup
OK - let's look at your markup. At the moment, you have almost got a table structure, because you're wrapping each row in a DIV. Again, this means you can't ever have a single column or 3 or 4 columns side-by-side.
A more flexible solution is to markup your data as a list. Because it's sorted, an ordered list is the right choice:
<ol id="list_properties">
<li class="property">
<h3 class="propertyTitle">08-CA-001731</h3>
<dl class="details">
<dt>Plantiff</dt>
<dd>Bob's Mortgages</dd>
<dt>Defendant</dt>
<dd>Harry Skinflint</dd>
</dl>
</li>
<li class="property">
<h3 class="propertyTitle">08-CA-001731</h3>
<dl class="details">
<dt>Plantiff</dt>
<dd>Bob's Mortgages</dd>
<dt>Defendant</dt>
<dd>Harry Skinflint</dd>
</dl>
</li>
</ol>
You'll have one <li class="property"> per item in your 'grid'. I've suggested some nicer markup for the details, too - help yourself to that if you like.
Now to finish it off: Sadly, this can't be done in a cross browser fashion without a bit of final tweaking with Javascript. That said, the JS is unobtrusive, so it really only acts as a bit of polish at the end.
Base styling
Your base styles will look like:
#list_properties {
list-style:none;
margin:0;
padding:0;
width:960px;
float:left;
}
#list_properties li.property{
width:430px;
float:left;
margin:0 50px 50px 0;
min-height:16em; /* For FF, Safari, Opera, IE8 */
_height:16em; /* For IE6 */
*height:16em; /* For IE7 */
}
That recreates the columns - at this stage, if all of the boxes had a height less than 16em, this would be all you need.
Javascript polish
To make sure everything is rock solid, I'd use the equal heights plugin for jQuery: http://www.cssnewbie.com/equalheights-jquery-plugin/ Basically, you pass it your container (#list_properties) and it scans each of the children (li.property), and sets the height of each one to the height of the tallest item.
So, for instance, one item has extra information and needs 18ems of height, all of the other items are set to 18em too.
The linked site has documentation for getting it going, but once you've got jquery and the plugin ready, you need only do:
$(function(){
$('#list_properties').equalHeights();
});
Once you get it set up this way, you can modify the number of items in the column just by changing the width of the li.property.
A last thought...
I'm just thinking, what is the advantage of displaying this list in columns, anyway? After all, the user can sort the list - wouldn't a single-column list be easier to scan? I'm assuming the user will be looking for a particular item, rather than just browsing through semi-randomly.
Floating <div> elements are tricky to work with, especially if you're aiming at multiple browsers.
If it's important that they line up in a grid format, I'd use a <table> instead; <div>s are meant for dynamic content that may or may not line up with everything else, and it seems like a misuse to expect them to line up perfectly, especially if their sizes are unspecified.
Also, since the float CSS property modifies how block-level elements interact with other floating and non-floating elements, I'd float the <table> element and give it a set width.
Check CSS display: table; for layout:
http://www.onenaught.com/posts/201/use-css-displaytable-for-layout
I think if you add a cleared element after every two entries that would help.
<div style="clear:both"></div>
That is, you have hp_prop twice then insert the cleared div.
The
<br class="clearboth">
is not having any effect in the position that you have them.
You might want to look into some of the grid-oriented CSS layout "frameworks" out there. The general idea is to have a bunch of classes that are applied to blocks of content so that you're not dropping little numbers all over the place. There are various different approaches; a google search for "CSS grid framework" should get you started.
Here's another idea: do you know how many tables are in a row? Well, can you not wrap each row of <div> boxes in another "row" <div>, with that one set to overflow: hidden so that it stretches out around the floated boxes inside it? The outer <div> elements would not need to float at all (though they could, if need be). That would have the effect of making each row as big as the biggest cell in it.
There are two options:
Mark the end of each "row" with a <div>or <hr> which "clears" both sides by using the css "clear" attribute. You can learn more about hit here: http://www.w3schools.com/Css/pr_class_clear.asp
Do what Pointy said and use a CSS framework. I would specifically recommend 960 Grid (http://960.gs) and/or BluePrint CSS (http://code.google.com/p/blueprintcss/). Both are simple, small, and easy to use. They basically use the above technique to achieve the result as well as offering you a bunch of other nice features (easy to create symmetrical and nicely proportioned layouts).
I have a table column that needs to be limited to a certain width - say 100 pixels. At times the text in that column is wider than this and contains no spaces. For example:
a_really_long_string_of_text_like_this_with_no_line_breaks_makes_the_table_unhappy
I would like to calculate the width of text server-side and add an ellipsis after the correct number of characters. The problem is that I don't have data about the rendered size of the text.
For example, assuming the browser was Firefox 3 and the font was 12px Arial. What would be the width of the letter "a", the width of the letter "b", etc.?
Do you have data showing the pixel width of each character? Or a program to generate it?
I think a clever one-time javascript script could do the trick. But I don't want to spend time re-inventing the wheel if someone else has already done this. I am surely not the first person to come up against this problem.
How about overflow: scroll?
Ext JS has a module to do just that
TextMetrics
Provides precise pixel measurements
for blocks of text so that you can
determine exactly how high and wide,
in pixels, a given block of text will
be.
I am sure that there are other libraries available out there that do it as well.
This would not only be impossible to do server-side, it would also not make sense. You don't what browser your client will be using, and you don't know what font settings on the client side will override whatever styling information you assign to a piece of HTML. You might think that you're using absolute positioning pixels in your style properties, but the client could simply be ignoring those or using some plugin to zoom everything because the client uses a high-dpi screen.
Using fixed widths is generally a bad idea.
Very very hard to do server-side. You can never know what fonts users have installed, and there are many things that affect the display of text.
Try this instead:
table-layout: fixed;
That'll make sure the table is never larger than the size you specified.
Here is my client-side solution that I came up with. It is pretty specific to my application but I am sharing it here in case someone else comes across the same problem.
It works a bit more quickly than I had expected. And it assumes the contents of the cells are text only - any HTML will formatting will be erased in the shortening process.
It requires jQuery.
function fixFatColumns() {
$('table#MyTable td').each(function() {
var defined_width = $(this).attr('width');
if (defined_width) {
var actual_width = $(this).width();
var contents = $(this).html();
if (contents.length) {
var working_div = $('#ATempDiv');
if (working_div.is('*')) {
working_div.html(contents);
} else {
$('body').append('<div id="ATempDiv" style="position:absolute;top:-100px;left:-500px;font-size:13px;font-family:Arial">'+contents+'</div>');
working_div = $('#ATempDiv');
}
if (working_div.width() > defined_width) {
contents = working_div.text();
working_div.text(contents);
while (working_div.width() + 8 > defined_width) {
// shorten the contents of the columns
var working_text = working_div.text();
if (working_text.length > 1) working_text = working_text.substr(0,working_text.length-1);
working_div.text(working_text);
}
$(this).html(working_text+'...')
}
working_div.empty();
}
}
});
}
This is essentially impossible to do on the server side. In addition to the problem of people having different fonts installed, you also have kerning (the letter "f" will take up a different amount of space depending on what is next to it) and font rendering options (is cleartype on? "large fonts"?).
There's nothing you can do server-side to calculate it. All you have to work with is the browser identification string, which may or may not tell you the user's operating system and browser accurately. You can also "ask" (via a font tag or CSS) for a certain font to be used to display the text but there's no guarantee that the user has that font installed. Beyond that the user could have a different DPI setting at the operating system level, or could have made the text bigger or smaller with the browser zoom function, or could be using their own stylesheet altogether.
You could put the text into an invisible span and read that spans width, but basicly this looks like someone trying to sabotage your site, and therefore I would recommend banning posts with words longer than a certain lenth, for example 30 characters without spaces (allowing links to be longer !-)
-- but the simple approach is to put a block-element inside the table-cell:
<td><div style="width:100px;overflow:hidden">a_really_long_string_of_text_like_this_with_no_line_breaks_makes_the_ta ... </div></td>
This will effectively stop the table-cluttering !o]
If you're ok with this not working for FireFox, why not just use CSS? Have the table with table-layout:fixed, have the column in question have overflow:hidden;text-overflow:ellipsis; white-space:nowrap.
http://www.css3.info/preview/text-overflow/
This is a new function of css3.
Some users have larger or smaller default font settings. You can't do this on the server. You can only measure it once the browser has rendered the page.
Since font size can be easily changed on the browser side, your server-side calculation is made invalid very easily.
A quick client side fix would be to style your cells with an overflow attribute:
td
{
overflow: scroll; /* or overflow: hidden; etc. */
}
A better alternative is to truncate your strings server side and provide a simple javascript tooltip that can display the longer version. An "expand" button may also help that could display the result in an overlay div.
What you want is the <wbr> tag. This is a special HTML tag that tells the browser that it is acceptable to break a word here if a wrap is necessary. I would not inject the into the text for persistent storage because then you are coupling your data with where/how you will display that data. However, it is perfectly acceptable to inject the tags server side in code that is view-centric (like with a JSP tag or possibly in the controller). That's how I would do it. Just use some regular expression to find words that are longer than X characters and inject this tag every X characters into such words.
Update: I was doing some looking around and it looks like wbr is not supported on all browsers. Most notably, IE8. I haven't tested this myself though. Perhaps you could use overflow:hidden as a backup or something like that.