vijithassar / d3-textwrap

painless cross-browser line wrapping for text passages in SVG projects built with D3.js
MIT License
154 stars 21 forks source link

wrap_with_tspans method omits single word at end #1

Open willw-git opened 10 years ago

willw-git commented 10 years ago

(About to report a number of bugs. Would first of all like to say thanks for a great effort - I really like the idea, and it is just what I need. Unfortunately...)

Using the wrap_with_tspans method with IE11, if a single word should be wrapped to a line by itself, then this word is missed out.

This capture shows this happening on the demo page:

wrap bug1

Adding an extra word to the text fixes it.

willw-git commented 10 years ago

Version of wrap_with_tspans() below

  1. Fixes single word on line issue, described above. Essentially the main loop 'forgot' the last word on a line by itself.
  2. Fixes text outdenting problem I described here https://github.com/vijithassar/d3textwrap/issues/2 I simply ignored all your complex line length calcs and bodged the text to start at the same place.
  3. I guess - untried - fixes the text appearing in wrong box issue https://github.com/vijithassar/d3textwrap/issues/3 - I think cause by failure to include the bounding box's (x,y) when calculating the start position for the text.

Excuse me not attempting a reverse fork merge thingy - I don't speak git.

            // wrap with tspans if foreignObject is undefined
            var wrap_with_tspans = function(item) {
                // operate on the first text item in the selection
                var text_node = item[0];
                var parent = text_node.parentNode;
                var text_node_selected = d3.select(text_node);
                // measure initial size of the text node as rendered
                var text_node_height = text_node.getBBox().height;
                var text_node_width = text_node.getBBox().width;
                // figure out the line height, either from rendered height
                // of the font or attached styling
                var line_height;
                var rendered_line_height = text_node_height;
                var styled_line_height = text_node_selected.style('line-height');
                if(
                    (styled_line_height) &&
                    (parseInt(styled_line_height))
                ) {
                    line_height = parseInt(styled_line_height.replace('px', ''));
                } else {
                    line_height = rendered_line_height;
                }
                // only fire the rest of this if the text content
                // overflows the desired dimensions
                if(text_node_width > bounds.width) {
                    // store whatever is inside the text node
                    // in a variable and then zero out the
                    // initial content; we'll reinsert in a moment
                    // using tspan elements.
                    var text_to_wrap = text_node_selected.text();
                    text_node_selected.text('');
                    if(text_to_wrap) {
                        // keep track of whether we are splitting by spaces
                        // so we know whether to reinsert those spaces later
                        var break_delimiter;
                        // split at spaces to create an array of individual words
                        var text_to_wrap_array;
                        if(text_to_wrap.indexOf(' ') !== -1) {
                            var break_delimiter = ' ';
                            text_to_wrap_array = text_to_wrap.split(' ');
                        } else {
                            // if there are no spaces, figure out the split
                            // points by comparing rendered text width against
                            // bounds and translating that into character position
                            // cuts
                            break_delimiter = '';
                            var string_length = text_to_wrap.length;
                            var number_of_substrings = Math.ceil(text_node_width / bounds.width);
                            var splice_interval = Math.floor(string_length / number_of_substrings);
                            if(
                                !(splice_interval * number_of_substrings >= string_length)
                            ) {
                                number_of_substrings++;
                            }
                            var text_to_wrap_array = [];
                            var substring;
                            var start_position;
                            for(var i = 0; i < number_of_substrings; i++) {
                                start_position = i * splice_interval;
                                substring = text_to_wrap.substr(start_position, splice_interval);
                                text_to_wrap_array.push(substring);
                            }
                        }

                        // new array where we'll store the words re-assembled into
                        // substrings that have been tested against the desired
                        // maximum wrapping width
                        var substrings = [];
                        // computed text length is arguably incorrectly reported for
                        // all tspans after the first one, in that they will include
                        // the width of previous separate tspans. to compensate we need
                        // to manually track the computed text length of all those
                        // previous tspans and substrings, and then use that to offset
                        // the miscalculation. this then gives us the actual correct
                        // position we want to use in rendering the text in the SVG.
                        var total_offset = 0;
                        // object for storing the results of text length computations later
                        var temp = {};
                        // loop through the words and test the computed text length
                        // of the string against the maximum desired wrapping width
                        for(var i = 0; i < text_to_wrap_array.length; i++) {
                            var word = text_to_wrap_array[i];
                            var previous_string = text_node_selected.text();
                            var previous_width = text_node.getComputedTextLength();
                            // initialize the current word as the first word
                            // or append to the previous string if one exists
                            var new_string;
                            if(previous_string) {
                                new_string = previous_string + break_delimiter + word;
                            } else {
                                new_string = word;
                            }
                            // add the newest substring back to the text node and
                            // measure the length
                            text_node_selected.text(new_string);
                            var new_width = text_node.getComputedTextLength();
                            // adjust the length by the offset we've tracked
                            // due to the misreported length discussed above
                            var test_width = new_width - total_offset;
                            // if our latest version of the string is too
                            // big for the bounds, use the previous
                            // version of the string (without the newest word
                            // added) and use the latest word to restart the
                            // process with a new tspan
                            if(new_width > bounds.width) {
                                if(
                                    (previous_string) &&
                                    (previous_string !== '')
                                ) {
                                    total_offset = total_offset + previous_width;
                                    temp = {string: previous_string, width: previous_width, offset: total_offset};
                                    substrings.push(temp);
                                    text_node_selected.text('');
                                    text_node_selected.text(word);
                                    // Handle case where there is just one more word to be wrapped
                                    if(i == text_to_wrap_array.length - 1) {
                                        new_string = word;
                                        text_node_selected.text(new_string);
                                        new_width = text_node.getComputedTextLength();
                                    }
                                }
                            }
                            // if we're up to the last word in the array,
                            // get the computed length as is without
                            // appending anything further to it
                            if(i == text_to_wrap_array.length - 1) {
                                text_node_selected.text('');
                                var final_string = new_string;
                                if(
                                    (final_string) &&
                                    (final_string !== '')
                                ) {
                                    if((new_width - total_offset) > 0) {new_width = new_width - total_offset}
                                    temp = {string: final_string, width: new_width, offset: total_offset};
                                    substrings.push(temp);
                                }
                            }
                        }

                        // append each substring as a tspan
                        var current_tspan;
                        var tspan_count;
                        // double check that the text content has been removed
                        // before we start appending tspans
                        text_node_selected.text('');
                        for(var i = 0; i < substrings.length; i++) {
                            var substring = substrings[i].string;
                            if(i > 0) {
                                var previous_substring = substrings[i - 1];
                            }
                            // only append if we're sure it won't make the tspans
                            // overflow the bounds.
                            if((i) * line_height < bounds.height - (line_height * 1.5)) {
                                current_tspan = text_node_selected.append('tspan')
                                    .text(substring);
                                // vertical shift to all tspans after the first one
                                current_tspan
                                    .attr('dy', function(d) {
                                        if(i > 0) {
                                            return line_height;
                                        }
                                    });
                                // shift left from default position, which
                                // is probably based on the full length of the
                                // text string until we make this adjustment
                                current_tspan
                                    .attr('x', function() {
                                        var x_offset = bounds.x;
                                        if(padding) {x_offset += padding;}
                                        return x_offset;
                                    });
//                                    .attr('dx', function() {
//                                        if(i == 0) {
//                                            var render_offset = 0;
//                                        } else if(i > 0) {
//                                            render_offset = substrings[i - 1].width;
//                                            render_offset = render_offset * -1;
//                                        }
//                                        return render_offset;
//                                    });
                            }
                        }
                    }
                }
                // position the overall text node, whether wrapped or not
                text_node_selected.attr('y', function() {
                    var y_offset = bounds.y;
                    // shift by line-height to move the baseline into
                    // the bounds – otherwise the text baseline would be
                    // at the top of the bounds
                    if(line_height) {y_offset += line_height;}
                    // shift by padding, if it's there
                    if(padding) {y_offset += padding;}
                    return y_offset;
                });
                // shift to the right by the padding value
                text_node_selected.attr('x', function() {
                    var x_offset = bounds.x;
                    if(padding) {x_offset += padding;}
                    return x_offset;
                });

                // assign our modified text node with tspans
                // to the return value
                return_value = d3.select(parent).selectAll('text');
            }
vijithassar commented 10 years ago

Wonderful! I'll take a closer look over the next few days and hopefully patch this bug shortly. The delay thus far has just been because I don't currently have an IE11 computer with which to test, so thanks for taking that off my hands.

willw-git commented 10 years ago

Fair enough. One thing I know still is not handled correctly: if you have a word which is so long that it does not fit on a line, then the div based solution clips it, whereas the IE version just lets it splurge out through the RH margin. I don't care - but you might ;-)