Thursday 4 September 2014

UX Smoke and Mirrors

I recently had to implement a requirement where the specification was (roughly):
1. Take a table of data and print it using a FlowDocument
2. The font size must decrease to fit as many columns as possible up to a minimum font size.

(Just a heads up, the next part is a bit boring. Feel free to skip to the last paragraph if you want to get to the point of the post quickly)

At first it seemed like a not very trivial thing to do. One approach I thought of was to loop through each column and each row, calculating the width of the text when rendered, therefore the expected width of each column. Then repeat that process, decreasing the font size, until all the columns fit or the minimum font size is reached. It's possible to calculate the expected rendered width of the column by creating a FormattedText object and getting the Width property.

This works, but it is immensely slow. If we have 100 rows with 100 columns, we're measuring the width of 10,000 strings, as many times as it takes to fit the data to the page. Not ideal.

Then I hit on a winner. What if I measure everything just once at the default font size and calculate the widths of each column. I have the width of the document so I know how many columns can fit. Here's where it gets interesting. A WPF Grid can be added to the document (it's possible to add a UIElement to a FlowDocument by adding it to a BlockUIContainer) and a scale transform can be applied to shrink it to fit the page (or as many columns as possible).

The scale ratio can be calculated by comparing the document width with the total column widths. There must be a minimum scale ratio (to ensure we don't minimise the font size too much) which can roughly be calculated by comparing the default font size and the minimum font size (took a bit of trial and error). So our scale ratio is the bigger of the calculated scale ratio and the minimum scale ratio.

Now it becomes relatively easy to add columns one at a time until no more columns will fit, using the column width and the scaling ratio in the calculations. Then create the Grid with the columns, apply the scale transform using the calculated ratio and add it to the document.

This worked like a dream. It was however still a bit slow for my liking. So I added logic to find the longest string in the column and calculate the width of that text. This made it lightning fast. Unfortunately it turns out not all text is created equal. Upper-case characters take up more space than lower-case so sometimes the data in a column was being truncated. To solve this I gave upper-case letters a slightly higher weighting than lower-case, compared the summed weightings of the text of each column to find what "should" be the longest text when rendered, then calculated the column width with that text.

All of that obviously super exciting for the reader. Maybe. And means very little to you with no code to look at. But the point I'm trying to make in this post is that with UX development, smoke and mirrors can sometimes be your best friend. If you can use a bit of magic to deliver your requirement and it does what the user expects, mission accomplished. And if your smoke and mirrors helps considerably with performance, even better.